diff --git a/TODO.md b/TODO.md index 4732599a..9081dded 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,7 @@ - [ ] codeBlock 在 list item 中时,list style 问题。 - [ ] 在通过 Aganippe 打开文件时,无法通过右键选择 Aganippe。(严重 bug) - [ ] 在通过 Aganippe 打开文件时,通过右键选择软件,但是打开无内容。(严重 bug) -- [ ] task item: (2)task item 只能和 task item 在一个 ul 中。所以需要优化 update 模块 +- [ ] task item: 第一行为 list item ,全选然后删除,youbug **菜单** diff --git a/src/editor/config.js b/src/editor/config.js index 3c5b9962..43a8a4dc 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -71,6 +71,10 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_REMOVE', 'AG_EMOJI_MARKER', 'AG_NOTEXT_LINK', + 'AG_ORDER_LIST', + 'AG_ORDER_LIST_ITEM', + 'AG_BULLET_LIST', + 'AG_BULLET_LIST_ITEM', 'AG_TASK_LIST', 'AG_TASK_LIST_ITEM', 'AG_TASK_LIST_ITEM_CHECKBOX' diff --git a/src/editor/contentState/backspaceCtrl.js b/src/editor/contentState/backspaceCtrl.js index 3ae7b3ac..9fb1b562 100644 --- a/src/editor/contentState/backspaceCtrl.js +++ b/src/editor/contentState/backspaceCtrl.js @@ -17,7 +17,7 @@ const backspaceCtrl = ContentState => { if ( (parent && parent.type === 'li' && inLeft === 0 && this.isFirstChild(block)) || - (parent && parent.type === 'li' && inLeft === 0 && parent.isTask && preBlock.type === 'input') // handle task item + (parent && parent.type === 'li' && inLeft === 0 && parent.listItemType === 'task' && preBlock.type === 'input') // handle task item ) { if (this.isOnlyChild(parent)) { /** diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index b79a6e01..fc4c7e23 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -27,7 +27,7 @@ const enterCtrl = ContentState => { const paragraphInListItem = this.createBlock('p', text) const checkboxInListItem = this.createBlock('input') - listItem.isTask = true + listItem.listItemType = 'task' checkboxInListItem.checked = checked this.appendChild(listItem, checkboxInListItem) this.appendChild(listItem, paragraphInListItem) @@ -96,7 +96,7 @@ const enterCtrl = ContentState => { event.preventDefault() if ( (parent && parent.type === 'li' && this.isOnlyChild(block)) || - (parent && parent.type === 'li' && parent.isTask && parent.children.length === 2) // one `input` and one `p` + (parent && parent.type === 'li' && parent.listItemType === 'task' && parent.children.length === 2) // one `input` and one `p` ) { block = parent parent = this.getParent(block) @@ -106,6 +106,7 @@ const enterCtrl = ContentState => { let type let newBlock + switch (true) { case left !== 0 && right !== 0: // cursor in the middle type = preType @@ -118,13 +119,14 @@ const enterCtrl = ContentState => { if (type === 'li') { // handle task item - if (block.isTask) { + if (block.listItemType === 'task') { const { checked } = block.children[0] // block.children[0] is input[type=checkbox] block.children[1].text = pre // block.children[1] is p newBlock = this.createTaskItemBlock(post, checked) } else { block.children[0].text = pre newBlock = this.createBlockLi(post) + newBlock.listItemType = block.listItemType } } else { block.text = pre @@ -150,11 +152,12 @@ const enterCtrl = ContentState => { this.removeBlock(block) } else if (parent && parent.type === 'li') { - if (parent.isTask) { + if (parent.listItemType === 'task') { const { checked } = parent.children[0] newBlock = this.createTaskItemBlock('', checked) } else { newBlock = this.createBlockLi() + newBlock.listItemType = parent.listItemType } this.insertAfter(newBlock, parent) const index = this.findIndex(parent.children, block) @@ -176,11 +179,12 @@ const enterCtrl = ContentState => { case left !== 0 && right === 0: // cursor at end of paragraph case left === 0 && right !== 0: // cursor at begin of paragraph if (preType === 'li') { - if (block.isTask) { + if (block.listItemType === 'task') { const { checked } = block.children[0] newBlock = this.createTaskItemBlock('', checked) } else { newBlock = this.createBlockLi() + newBlock.listItemType = block.listItemType } } else { newBlock = this.createBlock('p') @@ -205,7 +209,7 @@ const enterCtrl = ContentState => { const cursorBlock = blockNeedFocus ? block : newBlock let key if (cursorBlock.type === 'li') { - if (cursorBlock.isTask) { + if (cursorBlock.listItemType === 'task') { key = cursorBlock.children[1].key } else { key = cursorBlock.children[0].key diff --git a/src/editor/contentState/updateCtrl.js b/src/editor/contentState/updateCtrl.js index 482a9ff5..792a9200 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -31,7 +31,7 @@ const updateCtrl = ContentState => { ContentState.prototype.checkInlineUpdate = function (block) { const { text } = block const parent = this.getParent(block) - const [match, disorder, tasklist, order, header, blockquote, hr] = text.match(INLINE_UPDATE_REG) || [] + const [match, bullet, tasklist, order, header, blockquote, hr] = text.match(INLINE_UPDATE_REG) || [] let newType switch (true) { @@ -39,11 +39,11 @@ const updateCtrl = ContentState => { this.updateHr(block, hr) return true - case !!disorder: - this.updateList(block, 'disorder', disorder) + case !!bullet: + this.updateList(block, 'bullet', bullet) return true - case !!tasklist && parent && parent.type === 'li': + case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item this.updateTaskListItem(block, 'tasklist', tasklist) return true @@ -78,13 +78,49 @@ const updateCtrl = ContentState => { 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 const checkbox = this.createBlock('input') const { start, end } = this.cursor + checkbox.checked = checked this.insertBefore(checkbox, block) block.text = block.text.substring(marker.length) - parent.isTask = true + parent.listItemType = 'task' + + let taskListWrapper + if (this.isOnlyChild(parent)) { + grandpa.listType = 'task' + } else if (this.isFirstChild(parent) || this.isLastChild(parent)) { + taskListWrapper = this.createBlock('ul') + taskListWrapper.listType = 'task' + this.isFirstChild(parent) ? this.insertBefore(taskListWrapper, grandpa) : this.insertAfter(taskListWrapper, grandpa) + this.removeBlock(parent) + this.appendChild(taskListWrapper, parent) + } else { + taskListWrapper = this.createBlock('ul') + taskListWrapper.listType = 'task' + const bulletListWrapper = this.createBlock('ul') + bulletListWrapper.listType = 'bullet' + + let preSibling = this.getPreSibling(parent) + while (preSibling) { + this.removeBlock(preSibling) + if (bulletListWrapper.children.length) { + const firstChild = bulletListWrapper.children[0] + this.insertBefore(preSibling, firstChild) + } else { + this.appendChild(bulletListWrapper, preSibling) + } + preSibling = this.getPreSibling(preSibling) + } + + this.removeBlock(parent) + this.appendChild(taskListWrapper, parent) + this.insertBefore(taskListWrapper, grandpa) + this.insertBefore(bulletListWrapper, taskListWrapper) + } + this.cursor = { start: { key: start.key, @@ -105,47 +141,43 @@ const updateCtrl = ContentState => { } ContentState.prototype.updateList = function (block, type, marker) { - console.log(type, marker) const parent = this.getParent(block) const preSibling = this.getPreSibling(block) const nextSibling = this.getNextSibling(block) - const wrapperTag = type === 'order' ? 'ol' : 'ul' + const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol` const newText = block.text.substring(marker.length) const { start, end } = this.cursor const startOffset = start.offset const endOffset = end.offset - let newBlock + const newBlock = this.createBlockLi(newText) + newBlock.listItemType = type - if ((preSibling && preSibling.type === wrapperTag) && (nextSibling && nextSibling.type === wrapperTag)) { - newBlock = this.createBlockLi(newText) + if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) { this.appendChild(preSibling, newBlock) const partChildren = nextSibling.children.splice(0) partChildren.forEach(b => this.appendChild(preSibling, b)) this.removeBlock(nextSibling) this.removeBlock(block) } else if (preSibling && preSibling.type === wrapperTag) { - newBlock = this.createBlockLi(newText) this.appendChild(preSibling, newBlock) this.removeBlock(block) - } else if (nextSibling && nextSibling.type === wrapperTag) { - newBlock = this.createBlockLi(newText) + } else if (nextSibling && nextSibling.listType === type) { this.insertBefore(newBlock, nextSibling.children[0]) this.removeBlock(block) - } else if (parent && parent.type === wrapperTag) { - newBlock = this.createBlockLi(newText) + } else if (parent && parent.listType === type) { this.insertBefore(newBlock, block) this.removeBlock(block) } else { block.type = wrapperTag + block.listType = type // `bullet` or `order` block.text = '' if (wrapperTag === 'ol') { const start = marker.split('.')[0] block.start = start } - newBlock = this.createBlockLi(newText) this.appendChild(block, newBlock) } diff --git a/src/editor/parser/StateRender.js b/src/editor/parser/StateRender.js index e6b1bb1d..22a09dab 100644 --- a/src/editor/parser/StateRender.js +++ b/src/editor/parser/StateRender.js @@ -66,12 +66,40 @@ class StateRender { } if (block.children.length) { + if (/ul|ol/.test(block.type) && block.listType) { + switch (block.listType) { + case 'order': + blockSelector += `.${CLASS_OR_ID['AG_ORDER_LIST']}` + break + case 'bullet': + blockSelector += `.${CLASS_OR_ID['AG_BULLET_LIST']}` + break + case 'task': + blockSelector += `.${CLASS_OR_ID['AG_TASK_LIST']}` + break + default: + break + } + } + if (block.type === 'li' && block.listItemType) { + switch (block.listItemType) { + case 'order': + blockSelector += `.${CLASS_OR_ID['AG_ORDER_LIST_ITEM']}` + break + case 'bullet': + blockSelector += `.${CLASS_OR_ID['AG_BULLET_LIST_ITEM']}` + break + case 'task': + blockSelector += `.${CLASS_OR_ID['AG_TASK_LIST_ITEM']}` + break + default: + break + } + } if (block.type === 'ol') { Object.assign(data.attrs, { start: block.start }) } - if (block.type === 'li' && block.isTask) { - blockSelector += `.${CLASS_OR_ID['AG_TASK_LIST_ITEM']}` - } + return h(blockSelector, data, block.children.map(child => renderBlock(child))) } else { let children = block.text diff --git a/src/editor/parser/marked.js b/src/editor/parser/marked.js index 5f0746af..639f49a1 100644 --- a/src/editor/parser/marked.js +++ b/src/editor/parser/marked.js @@ -17,7 +17,9 @@ var block = { nptable: noop, lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, - list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + tasklist: /^( *)([*+-] \[(?:X|x|\s)\]) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1(?:[*+-] \[(?:X|x|\s)\]))\n*|\s*$)/, + orderlist: /^( *)(\d+\.) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1\d+\. )\n*|\s*$)/, + bulletlist: /^( *)([*+-]) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1[*+-] )\n*|\s*$)/, html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, table: noop, @@ -26,17 +28,18 @@ var block = { }; block.checkbox = /^\[([ x])\] +/; -block.bullet = /(?:[*+-]|\d+\.)/; +block.bullet = /(?:[*+-] \[(?:X|x|\s)\]|[*+-]|\d+\.)/; block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; block.item = replace(block.item, 'gm') (/bull/g, block.bullet) (); -block.list = replace(block.list) - (/bull/g, block.bullet) - ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') - ('def', '\\n+(?=' + block.def.source + ')') - (); +['tasklist', 'orderlist', 'bulletlist'].forEach(list => { + block[list] = replace(block[list]) + ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') + ('def', '\\n+(?=' + block.def.source + ')') + (); +}); block.blockquote = replace(block.blockquote) ('def', block.def) @@ -79,10 +82,13 @@ block.gfm = merge({}, block.normal, { heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ }); +// fix me block.gfm.paragraph = replace(block.paragraph) ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|' + - block.list.source.replace('\\1', '\\3') + '|') + block.tasklist.source.replace('\\1', '\\5') + '|' + + block.orderlist.source.replace('\\1', '\\7') + '|' + + block.bulletlist.source.replace('\\1', '\\9') + '|') (); /** @@ -168,7 +174,8 @@ Lexer.prototype.token = function(src, top, bq) { this.tokens.push({ type: 'code', text: !this.options.pedantic ? - cap.replace(/\n+$/, '') : cap + cap.replace(/\n+$/, '') : + cap }); continue; } @@ -270,18 +277,18 @@ Lexer.prototype.token = function(src, top, bq) { } // list - if (cap = this.rules.list.exec(src)) { + if (cap = this.rules.tasklist.exec(src) || this.rules.orderlist.exec(src) || this.rules.bulletlist.exec(src)) { src = src.substring(cap[0].length); bull = cap[2]; this.tokens.push({ type: 'list_start', - ordered: bull.length > 1 + ordered: bull.length > 1 && /\d/.test(bull), + listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet' }); // Get each top-level item. cap = cap[0].match(this.rules.item); - next = false; l = cap.length; i = 0; @@ -334,8 +341,10 @@ Lexer.prototype.token = function(src, top, bq) { this.tokens.push({ checked: checked, + listItemType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet', type: loose ? - 'loose_item_start' : 'list_item_start' + 'loose_item_start' : + 'list_item_start' }); // Recurse. @@ -358,7 +367,8 @@ Lexer.prototype.token = function(src, top, bq) { src = src.substring(cap[0].length); this.tokens.push({ type: this.options.sanitize ? - 'paragraph' : 'html', + 'paragraph' : + 'html', pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), text: cap[0] @@ -412,11 +422,13 @@ Lexer.prototype.token = function(src, top, bq) { // top-level paragraph if (top && (cap = this.rules.paragraph.exec(src))) { + console.log(cap) src = src.substring(cap[0].length); this.tokens.push({ type: 'paragraph', text: cap[1].charAt(cap[1].length - 1) === '\n' ? - cap[1].slice(0, -1) : cap[1] + cap[1].slice(0, -1) : + cap[1] }); continue; } @@ -812,16 +824,28 @@ Renderer.prototype.hr = function() { Renderer.prototype.list = function(body, ordered, taskList) { var type = ordered ? 'ol' : 'ul'; - var classes = taskList ? ' class="task-list"' : ''; + var classes = !ordered ? (taskList ? ' class="task-list"' : ' class="bullet-list"') : ' class="order-list"' return '<' + type + classes + '>\n' + body + '\n'; }; -Renderer.prototype.listitem = function(text, checked) { +Renderer.prototype.listitem = function(text, checked, listItemType) { + var classes + switch (listItemType) { + case 'order': + classes = ' class="order-list-item"' + break + case 'task': + classes = ' class="task-list-item"' + break + case 'bullet': + classes = ' class="bullet-list-item"' + break + } if (checked === undefined) { - return '
  • ' + text + '
  • \n'; + return '
  • ' + text + '
  • \n'; } - return '
  • ' + + return '
  • ' + ' ' + @@ -1074,7 +1098,8 @@ Parser.prototype.tok = function() { case 'list_item_start': { var body = '', - checked = this.token.checked; + checked = this.token.checked, + listItemType = this.token.listItemType; while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? @@ -1082,23 +1107,25 @@ Parser.prototype.tok = function() { this.tok(); } - return this.renderer.listitem(body, checked); + return this.renderer.listitem(body, checked, listItemType); } case 'loose_item_start': { var body = '', - checked = this.token.checked; + checked = this.token.checked, + listItemType = this.token.listItemType; while (this.next().type !== 'list_item_end') { body += this.tok(); } - return this.renderer.listitem(body, checked); + return this.renderer.listitem(body, checked, listItemType); } case 'html': { var html = !this.token.pre && !this.options.pedantic ? - this.inline.output(this.token.text) : this.token.text; + this.inline.output(this.token.text) : + this.token.text; return this.renderer.html(html); } case 'paragraph': diff --git a/src/editor/utils/exportMarkdown.js b/src/editor/utils/exportMarkdown.js index 5bfc66a1..473a4bbf 100644 --- a/src/editor/utils/exportMarkdown.js +++ b/src/editor/utils/exportMarkdown.js @@ -122,7 +122,7 @@ class ExportMarkdown { if (listInfo.type === 'ul') { itemMarker = '- ' - if (block.isTask) { + if (block.listItemType === 'task') { const firstChild = children[0] itemMarker += firstChild.checked ? '[x] ' : '[ ] ' children = children.slice(1) diff --git a/src/editor/utils/importMarkdown.js b/src/editor/utils/importMarkdown.js index aab29f7d..0b76c93a 100644 --- a/src/editor/utils/importMarkdown.js +++ b/src/editor/utils/importMarkdown.js @@ -49,7 +49,6 @@ const importRegister = ContentState => { } const htmlText = marked(markdown, { disableInline: true }) - const domAst = parse5.parseFragment(htmlText) const childNodes = domAst.childNodes @@ -100,7 +99,7 @@ const importRegister = ContentState => { const checked = child.attrs.some(attr => attr.name === 'checked' && attr.value === '') if (isTaskListItemCheckbox) { - parent.isTask = true // double check + parent.listItemType = 'task' // double check block = this.createBlock('input') block.checked = checked this.appendChild(parent, block) @@ -110,19 +109,22 @@ const importRegister = ContentState => { case 'li': const isTask = child.attrs.some(attr => attr.name === 'class' && attr.value === 'task-list-item') block = this.createBlock('li') - block.isTask = isTask + block.listItemType = parent.nodeName === 'ul' ? (isTask ? 'task' : 'bullet') : 'order' this.appendChild(parent, block) travel(block, child.childNodes) break case 'ul': + const isTaskList = child.attrs.some(attr => attr.name === 'class' && attr.value === 'task-list') block = this.createBlock('ul') + block.listType = isTaskList ? 'task' : 'bullet' travel(block, child.childNodes) this.appendChild(parent, block) break case 'ol': block = this.createBlock('ol') + block.listType = 'order' child.attrs.forEach(attr => { block[attr.name] = attr.value }) @@ -154,7 +156,8 @@ const importRegister = ContentState => { case '#text': const { parentNode } = child value = child.value - if (parentNode.nodeName === 'li' && value !== '\n') { + + if (parentNode.nodeName === 'li' && /\S/.test(value)) { block = this.createBlock('p', value) this.appendChild(parent, block) }