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 })
}