From 2e73062cb46fdc9fefdba9bd68cf85b400ce1bee Mon Sep 17 00:00:00 2001 From: Jocs Date: Thu, 1 Feb 2018 21:39:46 +0800 Subject: [PATCH] feat: align and delete in table tool bar --- TODO.md | 5 +- src/editor/assets/icons/align-center.svg | 1 + src/editor/assets/icons/align-left.svg | 1 + src/editor/assets/icons/align-right.svg | 1 + src/editor/assets/icons/delete.svg | 1 + src/editor/assets/icons/delete_1.svg | 1 + src/editor/assets/icons/table.svg | 1 + src/editor/config.js | 3 +- src/editor/contentState/enterCtrl.js | 17 ++-- src/editor/contentState/index.js | 16 ++-- src/editor/contentState/tableBlockCtrl.js | 95 +++++++++++++++++++++-- src/editor/contentState/updateCtrl.js | 2 +- src/editor/index.css | 48 ++++++++++++ src/editor/index.js | 18 +++++ src/editor/parser/StateRender.js | 29 ++++++- 15 files changed, 215 insertions(+), 24 deletions(-) create mode 100644 src/editor/assets/icons/align-center.svg create mode 100644 src/editor/assets/icons/align-left.svg create mode 100644 src/editor/assets/icons/align-right.svg create mode 100644 src/editor/assets/icons/delete.svg create mode 100644 src/editor/assets/icons/delete_1.svg create mode 100644 src/editor/assets/icons/table.svg diff --git a/TODO.md b/TODO.md index ffa9bbce..48fc40c5 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ - [ ] 在通过 Aganippe 打开文件时,无法通过右键选择 Aganippe。(严重 bug) - [ ] 在通过 Aganippe 打开文件时,通过右键选择软件,但是打开无内容。(严重 bug) - [ ] export html: (3) keyframe 和 font-face 以及 bar-top 的样式都可以删除。(4) 打包后的应用 axios 获取样式有问题。(5) 输出的 html 中 a 标签无法点击 +- [ ] table: 1. table 前面不能够点击出现光标。2. 处理 table 内容选中后的backspace, enter 等。 **菜单** @@ -151,11 +152,11 @@ _ 底线 **表格功能** -* [ ] 输入`|xxx|xxx|`回车或其他失去 active 的操作生成2 * 2 表格。如果是回车,p (1, 1)自动获取光标。 +* [x] 输入`|xxx|xxx|`回车或其他失去 active 的操作生成2 * 2 表格。如果是回车,p (1, 1)自动获取光标。 block 类型包括 table、thead、tr、th、tbody、td -* [ ] 处理表格内部的 enter、cmd + enter、backspace 键。 +* [x] 处理表格内部的 enter、cmd + enter、backspace 键。 enter 光标跳转到下一行第一个cell。如果已经是最后一行,光标跳转到下一的段落。 diff --git a/src/editor/assets/icons/align-center.svg b/src/editor/assets/icons/align-center.svg new file mode 100644 index 00000000..15cfe00d --- /dev/null +++ b/src/editor/assets/icons/align-center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/icons/align-left.svg b/src/editor/assets/icons/align-left.svg new file mode 100644 index 00000000..951c3d65 --- /dev/null +++ b/src/editor/assets/icons/align-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/icons/align-right.svg b/src/editor/assets/icons/align-right.svg new file mode 100644 index 00000000..77f1d1ce --- /dev/null +++ b/src/editor/assets/icons/align-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/icons/delete.svg b/src/editor/assets/icons/delete.svg new file mode 100644 index 00000000..1cb171eb --- /dev/null +++ b/src/editor/assets/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/icons/delete_1.svg b/src/editor/assets/icons/delete_1.svg new file mode 100644 index 00000000..7a127246 --- /dev/null +++ b/src/editor/assets/icons/delete_1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/icons/table.svg b/src/editor/assets/icons/table.svg new file mode 100644 index 00000000..30f3f3a6 --- /dev/null +++ b/src/editor/assets/icons/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/config.js b/src/editor/config.js index b9c1d927..fa9e2c50 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -77,7 +77,8 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_BULLET_LIST_ITEM', 'AG_TASK_LIST', 'AG_TASK_LIST_ITEM', - 'AG_TASK_LIST_ITEM_CHECKBOX' + 'AG_TASK_LIST_ITEM_CHECKBOX', + 'AG_TABLE_TOOL_BAR' ]) export const codeMirrorConfig = { diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index 6549fc7b..e83bcb6b 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -15,11 +15,15 @@ const enterCtrl = ContentState => { this.insertAfter(container, parent) } - ContentState.prototype.createRow = function (columns) { + ContentState.prototype.createRow = function (row) { const trBlock = this.createBlock('tr') + const len = row.children.length let i - for (i = 0; i < columns; i++) { + for (i = 0; i < len; i++) { const tdBlock = this.createBlock('td') + const preChild = row.children[i] + tdBlock.column = i + tdBlock.align = preChild.align this.appendChild(trBlock, tdBlock) } return trBlock @@ -110,13 +114,14 @@ const enterCtrl = ContentState => { if (!nextSibling) { const rowContainer = this.getBlock(row.parent) const table = this.getBlock(rowContainer.parent) + const figure = this.getBlock(table.parent) if (rowContainer.type === 'thead') { nextSibling = table.children[1] - } else if (table.nextSibling) { - nextSibling = this.getBlock(table.nextSibling) + } else if (figure.nextSibling) { + nextSibling = this.getBlock(figure.nextSibling) } else { nextSibling = this.createBlock('p') - this.insertAfter(nextSibling, table) + this.insertAfter(nextSibling, figure) } } return this.firstInDescendant(nextSibling) @@ -127,7 +132,7 @@ const enterCtrl = ContentState => { const rowContainer = this.getBlock(row.parent) if (event.metaKey) { - const nextRow = this.createRow(row.children.length) + const nextRow = this.createRow(row) if (rowContainer.type === 'thead') { const tBody = this.getBlock(rowContainer.nextSibling) this.insertBefore(nextRow, tBody.children[0]) diff --git a/src/editor/contentState/index.js b/src/editor/contentState/index.js index 661ed7de..28da433b 100644 --- a/src/editor/contentState/index.js +++ b/src/editor/contentState/index.js @@ -76,9 +76,9 @@ class ContentState { render () { const { blocks, cursor } = this - const activeBlockKey = this.getActiveBlockKey() + const activeBlocks = this.getActiveBlocks() - this.stateRender.render(blocks, cursor, activeBlockKey) + this.stateRender.render(blocks, cursor, activeBlocks) this.setCursor() this.pre2CodeMirror() console.log('render') @@ -237,13 +237,15 @@ class ContentState { remove(this.blocks, block) } - getActiveBlockKey () { - let block = this.getBlock(this.cursor.key) - if (!block) return null - while (block.parent) { + getActiveBlocks () { + let result = [] + let block = this.getBlock(this.cursor.start.key) + if (block) result.push(block) + while (block && block.parent) { block = this.getBlock(block.parent) + result.push(block) } - return block.key + return result } getCursorBlock () { diff --git a/src/editor/contentState/tableBlockCtrl.js b/src/editor/contentState/tableBlockCtrl.js index 2e1a808f..c6291199 100644 --- a/src/editor/contentState/tableBlockCtrl.js +++ b/src/editor/contentState/tableBlockCtrl.js @@ -1,9 +1,14 @@ import { isLengthEven } from '../utils' +import TableIcon from '../assets/icons/table.svg' +import LeftIcon from '../assets/icons/align-left.svg' +import CenterIcon from '../assets/icons/align-center.svg' +import RightIcon from '../assets/icons/align-right.svg' +import DeleteIcon from '../assets/icons/delete.svg' const TABLE_BLOCK_REG = /^\|.*?(\\*)\|.*?(\\*)\|/ const tableBlockCtrl = ContentState => { - ContentState.prototype.createTable = function (block) { + ContentState.prototype.initTable = function (block) { const { text } = block const rowHeader = [] const len = text.length @@ -22,6 +27,7 @@ const tableBlockCtrl = ContentState => { } } const colLen = rowHeader.length + const table = this.createBlock('table') const tHead = this.createBlock('thead') const headRow = this.createBlock('tr') const tBody = this.createBlock('tbody') @@ -31,23 +37,102 @@ const tableBlockCtrl = ContentState => { for (i = 0; i < colLen; i++) { const headCell = this.createBlock('th', rowHeader[i]) const bodyCell = this.createBlock('td') + headCell.column = i + headCell.align = '' + bodyCell.column = i + bodyCell.align = '' + this.appendChild(headRow, headCell) this.appendChild(bodyRow, bodyCell) if (i === 0) result = bodyCell } - block.type = 'table' + this.appendChild(table, tHead) + this.appendChild(table, tBody) + + const toolBar = this.createBlock('div') + toolBar.editable = false + const ul = this.createBlock('ul') + const tools = [{ + label: 'table', + icon: TableIcon + }, { + label: 'left', + icon: LeftIcon + }, { + label: 'center', + icon: CenterIcon + }, { + label: 'right', + icon: RightIcon + }, { + label: 'delete', + icon: DeleteIcon + }] + + tools.forEach(tool => { + const toolBlock = this.createBlock('li') + const imgBlock = this.createBlock('img') + imgBlock.src = tool.icon + toolBlock.label = tool.label + this.appendChild(toolBlock, imgBlock) + this.appendChild(ul, toolBlock) + }) + this.appendChild(toolBar, ul) + + block.type = 'figure' block.text = '' block.children = [] - this.appendChild(block, tHead) - this.appendChild(block, tBody) + this.appendChild(block, toolBar) + this.appendChild(block, table) return result } + ContentState.prototype.tableToolBarClick = function (type) { + const { start: { key } } = this.cursor + const block = this.getBlock(key) + if (!(/td|th/.test(block.type))) throw new Error('table is not active') + const { column, align } = block + const getTable = td => { + const row = this.getBlock(block.parent) + const rowContainer = this.getBlock(row.parent) + return this.getBlock(rowContainer.parent) + } + const table = getTable(block) + switch (type) { + case 'left': + case 'center': + case 'right': { + const newAlign = align === type ? '' : type + table.children.forEach(rowContainer => { + rowContainer.children.forEach(row => { + row.children[column].align = newAlign + }) + }) + this.render() + break + } + case 'delete': { + const figure = this.getBlock(table.parent) + figure.children = [] + figure.type = 'p' + figure.text = '' + const key = figure.key + const offset = 0 + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + this.render() + break + } + } + } + ContentState.prototype.tableBlockUpdate = function (block) { const { type, text } = block if (type !== 'li' && type !== 'p') return false const match = TABLE_BLOCK_REG.exec(text) - return (match && isLengthEven(match[1]) && isLengthEven(match[2])) ? this.createTable(block) : false + return (match && isLengthEven(match[1]) && isLengthEven(match[2])) ? this.initTable(block) : false } } diff --git a/src/editor/contentState/updateCtrl.js b/src/editor/contentState/updateCtrl.js index e275167c..0376a7cc 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -29,7 +29,7 @@ const updateCtrl = ContentState => { } ContentState.prototype.checkInlineUpdate = function (block) { - if (/th|td/.test(block.type)) return false + if (/th|td|figure/.test(block.type)) return false const { text } = block const parent = this.getParent(block) const [match, bullet, tasklist, order, header, blockquote, hr] = text.match(INLINE_UPDATE_REG) || [] diff --git a/src/editor/index.css b/src/editor/index.css index f647f3d5..8eddbf64 100644 --- a/src/editor/index.css +++ b/src/editor/index.css @@ -21,10 +21,58 @@ h6.ag-active::before { font-weight: 100; } +figure { + padding: 0; + margin: 0; + margin-top: 18px; + position: relative; +} +.ag-table-tool-bar { + user-select: none; + position: absolute; + top: -20px; + left: 0; + display: none; +} +.ag-table-tool-bar ul { + height: 18px; + list-style: none; + margin: 0; + padding: 0; + display: flex; +} +.ag-table-tool-bar ul li { + box-sizing: border-box; + display: flex; + width: 18px; + height: 18px; + padding: 2px; + margin-right: 3px; + cursor: pointer; + border-radius: 3px; +} +.ag-table-tool-bar ul li img { + width: 14px; + height: 14px; +} + +.ag-table-tool-bar ul li.active { + background: lightblue; +} + +.ag-table-tool-bar ul li:hover { + background: #bbb; +} + +figure.ag-active .ag-table-tool-bar { + display: block; +} + table { width: 100%; border-collapse: collapse; border: 1px solid #ebeef5; + margin-top: 0; } a { diff --git a/src/editor/index.js b/src/editor/index.js index dbc85999..96fa7d1c 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -58,6 +58,7 @@ class Aganippe { this.dispatchEnter() this.dispatchUpdateState() this.dispatchCopyCut() + this.dispatchTableToolBar() } /** @@ -269,9 +270,26 @@ class Aganippe { eventCenter.attachDOMEvent(container, 'keydown', handler) } + dispatchTableToolBar () { + const { container, eventCenter } = this + const handler = event => { + const target = event.target + const parent = target.parentNode + if (parent && parent.hasAttribute('data-label')) { + const type = parent.getAttribute('data-label') + this.contentState.tableToolBarClick(type) + } + } + + eventCenter.attachDOMEvent(container, 'click', handler) + } + dispatchUpdateState () { const { container, eventCenter } = this const changeHandler = event => { + // const target = event.target + // const style = getComputedStyle(target) + // if (event.type === 'click' && !style.contenteditable) return if (!this._isEditChinese || event.type === 'input') { this.contentState.updateState(event) } diff --git a/src/editor/parser/StateRender.js b/src/editor/parser/StateRender.js index 7691a571..641ec3f3 100644 --- a/src/editor/parser/StateRender.js +++ b/src/editor/parser/StateRender.js @@ -49,12 +49,12 @@ class StateRender { * [render]: 2 steps: * render vdom */ - render (blocks, cursor, activeBlockKey) { + render (blocks, cursor, activeBlocks) { const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}` const renderBlock = block => { const type = block.type === 'hr' ? 'p' : block.type - const isActive = block.key === activeBlockKey || block.key === cursor.start.key + const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key let blockSelector = isActive ? `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}.${CLASS_OR_ID['AG_ACTIVE']}` @@ -66,6 +66,10 @@ class StateRender { } if (block.children.length) { + if (/div/.test(block.type) && !block.editable) { + blockSelector += `.${CLASS_OR_ID['AG_TABLE_TOOL_BAR']}` + Object.assign(data.attrs, { contenteditable: 'false' }) + } if (/ul|ol/.test(block.type) && block.listType) { switch (block.listType) { case 'order': @@ -81,6 +85,15 @@ class StateRender { break } } + if (block.type === 'li' && block.label) { + const { label } = block + const { align } = activeBlocks[0] + + if (align && block.label === align) { + blockSelector += '.active' + } + Object.assign(data.dataset, { label }) + } if (block.type === 'li' && block.listItemType) { switch (block.listItemType) { case 'order': @@ -109,6 +122,18 @@ class StateRender { }, []) : [ h(LOWERCASE_TAGS.br) ] + if (/th|td/.test(block.type)) { + const { align } = block + if (align) { + Object.assign(data.attrs, { style: `text-align:${align}` }) + } + } + + if (/img/.test(block.type)) { + const { src } = block + Object.assign(data.attrs, { src }) + children = '' + } if (/^h\d$/.test(block.type)) { Object.assign(data.dataset, { head: block.type }) }