diff --git a/TODO.md b/TODO.md index f6fe5c58..5de8ebf8 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ - [ ] 在通过 Aganippe 打开文件时,通过右键选择软件,但是打开无内容。(严重 bug) - [ ] export html: (3) keyframe 和 font-face 以及 bar-top 的样式都可以删除。(4) 打包后的应用 axios 获取样式有问题。 - [ ] table: 如果 table 在 selection 后面,那么删除cell 的时候,会把整个 row 删除了。(小 bug) +- [ ] task list 中 checkbox 无反应 **菜单** diff --git a/src/editor/config.js b/src/editor/config.js index 6be736b0..165f3c67 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -6,7 +6,7 @@ import { generateKeyHash, genUpper2LowerKeyHash } from './utils' * configs */ // export const INLINE_RULES = ['autolink', 'backticks', 'emphasis', 'escape', 'image', 'link', 'strikethrough'] - +export const PARAGRAPH_TYPES = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'ol', 'li', 'figure'] export const blockContainerElementNames = [ // elements our editor generates 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol', diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index babc7b84..645d368d 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -29,9 +29,9 @@ const enterCtrl = ContentState => { return trBlock } - ContentState.prototype.createBlockLi = function (text = '') { + ContentState.prototype.createBlockLi = function (text = '', type = 'p') { const liBlock = this.createBlock('li') - const pBlock = this.createBlock('p', text) + const pBlock = this.createBlock(type, text) this.appendChild(liBlock, pBlock) return liBlock } diff --git a/src/editor/contentState/index.js b/src/editor/contentState/index.js index d4a05482..ebcb045c 100644 --- a/src/editor/contentState/index.js +++ b/src/editor/contentState/index.js @@ -135,7 +135,7 @@ class ContentState { } return null } - + // return block and its parents getParents (block) { const result = [] result.push(block) diff --git a/src/editor/contentState/paragraphCtrl.js b/src/editor/contentState/paragraphCtrl.js index c74c6097..5cc2705b 100644 --- a/src/editor/contentState/paragraphCtrl.js +++ b/src/editor/contentState/paragraphCtrl.js @@ -1,5 +1,10 @@ import selection from '../selection' +import { PARAGRAPH_TYPES } from '../config' +import ExportMarkdown from '../utils/exportMarkdown' +// get header level +// eg: h1 => 1 +// h2 => 2 const getCurrentLevel = type => { if (/\d/.test(type)) { return Number(/\d/.exec(type)[0]) @@ -11,14 +16,16 @@ const getCurrentLevel = type => { const paragraphCtrl = ContentState => { ContentState.prototype.selectionChange = function () { const { start, end } = selection.getCursorRange() - start.type = this.getBlock(start.key).type - end.type = this.getBlock(end.key).type const startBlock = this.getBlock(start.key) const endBlock = this.getBlock(end.key) const startParents = this.getParents(startBlock) const endParents = this.getParents(endBlock) + const affiliation = startParents + .filter(p => endParents.includes(p)) + .filter(p => PARAGRAPH_TYPES.includes(p.type)) - const affiliation = startParents.filter(p => endParents.includes(p)) + start.type = startBlock.type + end.type = endBlock.type return { start, @@ -27,16 +34,212 @@ const paragraphCtrl = ContentState => { } } + ContentState.prototype.getCommonParent = function () { + const { start, end, affiliation } = this.selectionChange() + const parent = affiliation.length ? affiliation[0] : null + const startBlock = this.getBlock(start.key) + const endBlock = this.getBlock(end.key) + const startParentKeys = this.getParents(startBlock).map(b => b.key) + const endParentKeys = this.getParents(endBlock).map(b => b.key) + const children = parent ? parent.children : this.blocks + let startIndex + let endIndex + for (const child of children) { + if (startParentKeys.includes(child.key)) { + startIndex = children.indexOf(child) + } + if (endParentKeys.includes(child.key)) { + endIndex = children.indexOf(child) + } + } + return { parent, startIndex, endIndex } + } + + ContentState.prototype.handleListMenu = function (paraType) { + const { start, end, affiliation } = this.selectionChange() + const [blockType, listType] = paraType.split('-') + const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type)) + + if (isListed.length && listType !== isListed[0].listType) { + const listBlock = isListed[0] + // if the old list block is task list, remove checkbox + if (listBlock.listType === 'task') { + const listItems = listBlock.children + listItems.forEach(item => { + const inputBlock = item.children[0] + inputBlock && this.removeBlock(inputBlock) + }) + } + listBlock.type = blockType + listBlock.listType = listType + listBlock.children.forEach(b => (b.listItemType = listType)) + + if (listType === 'order') { + listBlock.start = listBlock.start || 1 + } + // if the new block is task list, add checkbox + if (listType === 'task') { + const listItems = listBlock.children + listItems.forEach(item => { + const checkbox = this.createBlock('input') + checkbox.checked = false + this.insertBefore(checkbox, item.children[0]) + }) + } + } else { + if (start.key === end.key) { + const block = this.getBlock(start.key) + if (listType === 'task') { + // 1. first update the block to bullet list + const listItemParagraph = this.updateList(block, 'bullet') + // 2. second update bullet list to task list + setTimeout(() => { + this.updateTaskListItem(listItemParagraph, listType) + this.render() + }) + } else { + this.updateList(block, listType) + } + } else { + const { parent, startIndex, endIndex } = this.getCommonParent() + const children = parent ? parent.children : this.blocks + const referBlock = children[endIndex] + const listWrapper = this.createBlock(listType === 'order' ? 'ol' : 'ul') + listWrapper.listType = listType + this.insertAfter(listWrapper, referBlock) + if (listType === 'order') listWrapper.start = 1 + let i + let child + const removeCache = [] + for (i = startIndex; i <= endIndex; i++) { + child = children[i] + removeCache.push(child) + const listItem = this.createBlock('li') + listItem.listItemType = listType + this.appendChild(listWrapper, listItem) + if (listType === 'task') { + const checkbox = this.createBlock('input') + checkbox.checked = false + this.appendChild(listItem, checkbox) + } + this.appendChild(listItem, child) + } + removeCache.forEach(b => this.removeBlock(b)) + } + } + } + + ContentState.prototype.handleCodeBlockMenu = function () { + const { start, end, affiliation } = this.selectionChange() + const startBlock = this.getBlock(start.key) + const endBlock = this.getBlock(end.key) + const startParents = this.getParents(startBlock) + const endParents = this.getParents(endBlock) + const hasPreParent = () => { + return startParents.some(b => b.type === 'pre') || endParents.some(b => b.type === 'pre') + } + if (affiliation.length && affiliation[0].type === 'pre') { + const codeBlock = affiliation[0] + delete codeBlock.pos + delete codeBlock.history + delete codeBlock.lang + this.codeBlocks.delete(codeBlock.key) + codeBlock.type = 'p' + const key = codeBlock.key + const offset = codeBlock.text.length + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + } else { + if (start.key === end.key) { + startBlock.type = 'pre' + startBlock.history = null + startBlock.lang = '' + } else if (!hasPreParent()) { + const { parent, startIndex, endIndex } = this.getCommonParent() + const children = parent ? parent.children : this.blocks + const referBlock = children[endIndex] + const codeBlock = this.createBlock('pre') + codeBlock.history = null + codeBlock.lang = '' + const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate() + + codeBlock.text = markdown + this.insertAfter(codeBlock, referBlock) + let i + const removeCache = [] + for (i = startIndex; i <= endIndex; i++) { + const child = children[i] + removeCache.push(child) + } + removeCache.forEach(b => this.removeBlock(b)) + const key = codeBlock.key + this.cursor = { + start: { key, offset: 0 }, + end: { key, offset: 0 } + } + } + } + } + + ContentState.prototype.handleQuoteMenu = function () { + const { start, end, affiliation } = this.selectionChange() + const startBlock = this.getBlock(start.key) + const isBlockQuote = affiliation.slice(0, 2).filter(b => /blockquote/.test(b.type)) + if (isBlockQuote.length) { + const quoteBlock = isBlockQuote[0] + console.log(quoteBlock) + const children = quoteBlock.children + for (const child of children) { + this.insertBefore(child, quoteBlock) + } + this.removeBlock(quoteBlock) + } else { + if (start.key === end.key) { + const quoteBlock = this.createBlock('blockquote') + this.insertAfter(quoteBlock, startBlock) + this.removeBlock(startBlock) + this.appendChild(quoteBlock, startBlock) + } else { + const { parent, startIndex, endIndex } = this.getCommonParent() + const children = parent ? parent.children : this.blocks + const referBlock = children[endIndex] + const quoteBlock = this.createBlock('blockquote') + this.insertAfter(quoteBlock, referBlock) + let i + const removeCache = [] + for (i = startIndex; i <= endIndex; i++) { + const child = children[i] + removeCache.push(child) + this.appendChild(quoteBlock, child) + } + + removeCache.forEach(b => this.removeBlock(b)) + } + } + } + ContentState.prototype.updateParagraph = function (paraType) { - const { - start, end - } = selection.getCursorRange() + const { start, end } = selection.getCursorRange() const block = this.getBlock(start.key) - const { - type, text - } = block + const { type, text } = block switch (paraType) { + case 'ul-bullet': + case 'ul-task': + case 'ol-order': { + this.handleListMenu(paraType) + break + } + case 'pre': { + this.handleCodeBlockMenu() + break + } + case 'blockquote': { + this.handleQuoteMenu() + break + } case 'heading 1': case 'heading 2': case 'heading 3': diff --git a/src/editor/contentState/updateCtrl.js b/src/editor/contentState/updateCtrl.js index a59b9b31..28198341 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -78,7 +78,7 @@ const updateCtrl = ContentState => { return false } - ContentState.prototype.updateTaskListItem = function (block, type, marker) { + ContentState.prototype.updateTaskListItem = function (block, type, marker = '') { const parent = this.getParent(block) const grandpa = this.getParent(parent) const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case @@ -142,7 +142,7 @@ const updateCtrl = ContentState => { block.checked = checked } - ContentState.prototype.updateList = function (block, type, marker) { + ContentState.prototype.updateList = function (block, type, marker = '') { const parent = this.getParent(block) const preSibling = this.getPreSibling(block) const nextSibling = this.getNextSibling(block) @@ -151,7 +151,7 @@ const updateCtrl = ContentState => { const { start, end } = this.cursor const startOffset = start.offset const endOffset = end.offset - const newBlock = this.createBlockLi(newText) + const newBlock = this.createBlockLi(newText, block.type) newBlock.listItemType = type if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) { @@ -178,7 +178,7 @@ const updateCtrl = ContentState => { block.text = '' if (wrapperTag === 'ol') { const start = marker.split('.')[0] - block.start = start + block.start = /^\d+$/.test(start) ? start : 1 } this.appendChild(block, newBlock) } @@ -194,6 +194,7 @@ const updateCtrl = ContentState => { offset: Math.max(0, endOffset - marker.length) } } + return newBlock.children[0] } ContentState.prototype.updateBlockQuote = function (block) { diff --git a/src/main/actions/paragraph.js b/src/main/actions/paragraph.js index 976058c9..39ba4145 100644 --- a/src/main/actions/paragraph.js +++ b/src/main/actions/paragraph.js @@ -16,7 +16,7 @@ const LABEL_MAP = { 'Heading 6': 'h6', 'Table': 'figure', 'Code Fences': 'pre', - 'Quote Block': 'quoteblock', + 'Quote Block': 'blockquote', 'Order List': 'ol', 'Bullet List': 'ul', 'Task List': 'ul', @@ -70,7 +70,7 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => { setCheckedMenuItem(affiliation) // handle disable allCtrl(true) - if (/th|td/.test(start.type) || /th|td/.test(end.type)) { + if (/th|td/.test(start.type) && /th|td/.test(end.type)) { allCtrl(false) } else if (start.key !== end.key) { formatMenuItem.submenu.items diff --git a/src/main/menus/paragraph.js b/src/main/menus/paragraph.js index 3bbb5da6..1b2f2d0a 100644 --- a/src/main/menus/paragraph.js +++ b/src/main/menus/paragraph.js @@ -72,14 +72,14 @@ export default { type: 'checkbox', accelerator: 'Alt+CmdOrCtrl+C', click (menuItem, browserWindow) { - // + actions.paragraph(browserWindow, 'pre') } }, { label: 'Quote Block', type: 'checkbox', accelerator: 'Alt+CmdOrCtrl+Q', click (menuItem, browserWindow) { - // + actions.paragraph(browserWindow, 'blockquote') } }, { type: 'separator' @@ -88,21 +88,21 @@ export default { type: 'checkbox', accelerator: 'Alt+CmdOrCtrl+O', click (menuItem, browserWindow) { - // + actions.paragraph(browserWindow, 'ol-order') } }, { label: 'Bullet List', type: 'checkbox', accelerator: 'Alt+CmdOrCtrl+U', click (menuItem, browserWindow) { - // + actions.paragraph(browserWindow, 'ul-bullet') } }, { label: 'Task List', type: 'checkbox', accelerator: 'Alt+CmdOrCtrl+X', click (menuItem, browserWindow) { - // + actions.paragraph(browserWindow, 'ul-task') } }, { type: 'separator' diff --git a/src/renderer/components/Editor.vue b/src/renderer/components/Editor.vue index d7d76ab8..b3701b5b 100644 --- a/src/renderer/components/Editor.vue +++ b/src/renderer/components/Editor.vue @@ -92,12 +92,16 @@ } }, handleEditParagraph (type) { - console.log(type) switch (type) { case 'table': this.tableChecker = { rows: 2, columns: 2 } this.dialogTableVisible = true break + case 'ul-bullet': + case 'ul-task': + case 'ol-order': + case 'pre': + case 'blockquote': case 'heading 1': case 'heading 2': case 'heading 3':