diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 19bc3504..dd3741e0 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,3 +1,22 @@ +### 0.11.8 + +**:cactus:Feature** + +- feature: add editorFont setting in user preference. (#175) - Anderson + +**:butterfly:Optimization** + +- #177 ATX headings strictly follow the GFM Spec - Jocs +- no need to auto pair when * is to open a list item - Jocs +- optimization: add sticky to block html tag - Jocs +- Add Japanese readme (#191) - Neetshin + +**:beetle:Bug fix** + +- fix the error 'Cannot read property 'forEach' of undefined' (#178) - 鸿则 +- fix: Change Source Code Mode Accelerator (#180) - Mice +- fix: #153 Double space between tasklist checkbox and text - Jocs + ### 0.10.21 **:notebook_with_decorative_cover:​Note** diff --git a/src/editor/assets/icons/enter.svg b/src/editor/assets/icons/enter.svg new file mode 100644 index 00000000..8a0c105b --- /dev/null +++ b/src/editor/assets/icons/enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/config.js b/src/editor/config.js index 4a15d9c4..0caf2394 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -60,6 +60,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_HIDE', 'AG_WARN', 'AG_PARAGRAPH', // => 'ag-paragraph' + 'AG_LINE', 'AG_ACTIVE', 'AG_EDITOR_ID', 'AG_FLOAT_BOX_ID', @@ -108,7 +109,8 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_LOOSE_LIST_ITEM', 'AG_TIGHT_LIST_ITEM', 'AG_HTML_TAG', - 'AG_A_LINK' + 'AG_LINK', + 'AG_HARD_LINE_BREAK' ]) export const codeMirrorConfig = { diff --git a/src/editor/contentState/arrowCtrl.js b/src/editor/contentState/arrowCtrl.js index 34faa3f7..57973022 100644 --- a/src/editor/contentState/arrowCtrl.js +++ b/src/editor/contentState/arrowCtrl.js @@ -3,7 +3,7 @@ import { isCursorAtFirstLine, isCursorAtLastLine, isCursorAtBegin, isCursorAtEnd import { findNearestParagraph } from '../utils/domManipulate' import selection from '../selection' -const HAS_TEXT_BLOCK_REG = /^(h\d|p|th|td|hr|pre)/ +const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/ const arrowCtrl = ContentState => { ContentState.prototype.firstInDescendant = function (block) { @@ -119,7 +119,7 @@ const arrowCtrl = ContentState => { ) { activeBlock = preBlock if (/^(?:pre|th|td)$/.test(preBlock.type)) { - activeBlock = this.createBlock('p') + activeBlock = this.createBlockP() activeBlock.temp = true this.insertBefore(activeBlock, anchorBlock) } @@ -134,12 +134,12 @@ const arrowCtrl = ContentState => { if (nextBlock) { activeBlock = nextBlock if (/^(?:pre|th|td)$/.test(nextBlock.type)) { - activeBlock = this.createBlock('p') + activeBlock = this.createBlockP() activeBlock.temp = true this.insertAfter(activeBlock, anchorBlock) } } else { - activeBlock = this.createBlock('p') + activeBlock = this.createBlockP() this.insertAfter(activeBlock, anchorBlock) } } @@ -147,8 +147,9 @@ const arrowCtrl = ContentState => { } if (activeBlock) { - const offset = activeBlock.text.length - const key = activeBlock.key + const cursorBlock = activeBlock.type === 'p' ? activeBlock.children[0] : activeBlock + const offset = cursorBlock.text.length + const key = cursorBlock.key this.cursor = { start: { key, @@ -165,13 +166,13 @@ const arrowCtrl = ContentState => { } if (/th|td/.test(block.type)) { let activeBlock - const anchorBlock = this.getParent(this.getParent(this.getParent(this.getParent(block)))) + const anchorBlock = this.getParent(this.getParent(this.getParent(this.getParent(block)))) // figure if ( (block.type === 'th' && preBlock && /^(?:pre|td)$/.test(preBlock.type) && event.key === EVENT_KEYS.ArrowUp) || (block.type === 'th' && preBlock && /^(?:pre|td)$/.test(preBlock.type) && event.key === EVENT_KEYS.ArrowLeft && left === 0) ) { - activeBlock = this.createBlock('p') + activeBlock = this.createBlockP() activeBlock.temp = true this.insertBefore(activeBlock, anchorBlock) } @@ -180,14 +181,14 @@ const arrowCtrl = ContentState => { (block.type === 'td' && nextBlock && /^(?:pre|th)$/.test(nextBlock.type) && event.key === EVENT_KEYS.ArrowDown) || (block.type === 'td' && nextBlock && /^(?:pre|th)$/.test(nextBlock.type) && event.key === EVENT_KEYS.ArrowRight && right === 0) ) { - activeBlock = this.createBlock('p') + activeBlock = this.createBlockP() activeBlock.temp = true this.insertAfter(activeBlock, anchorBlock) } if (activeBlock) { event.preventDefault() const offset = 0 - const key = activeBlock.key + const key = activeBlock.children[0].key this.cursor = { start: { key, @@ -238,10 +239,9 @@ const arrowCtrl = ContentState => { (event.key === EVENT_KEYS.ArrowLeft && start.offset === 0) ) { event.preventDefault() - const preBlockInLocation = this.findPreBlockInLocation(block) - if (!preBlockInLocation) return - const key = preBlockInLocation.key - const offset = preBlockInLocation.text.length + if (!preBlock) return + const key = preBlock.key + const offset = preBlock.text.length this.cursor = { start: { key, offset }, end: { key, offset } @@ -252,15 +252,14 @@ const arrowCtrl = ContentState => { (event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length) ) { event.preventDefault() - const nextBlockInLocation = this.findNextBlockInLocation(block) let key - if (nextBlockInLocation) { - key = nextBlockInLocation.key + if (nextBlock) { + key = nextBlock.key } else { - const newBlock = this.createBlock('p') + const newBlock = this.createBlockP() const lastBlock = this.blocks[this.blocks.length - 1] this.insertAfter(newBlock, lastBlock) - key = newBlock.key + key = newBlock.children[0].key } const offset = 0 this.cursor = { diff --git a/src/editor/contentState/backspaceCtrl.js b/src/editor/contentState/backspaceCtrl.js index 1e5d5b80..08e7c94a 100644 --- a/src/editor/contentState/backspaceCtrl.js +++ b/src/editor/contentState/backspaceCtrl.js @@ -7,7 +7,8 @@ const backspaceCtrl = ContentState => { const node = selection.getSelectionStart() const nearestParagraph = findNearestParagraph(node) const outMostParagraph = findOutMostParagraph(node) - const block = this.getBlock(nearestParagraph.id) + let block = this.getBlock(nearestParagraph.id) + if (block.type === 'span') block = this.getParent(block) const preBlock = this.getPreSibling(block) const outBlock = this.findOutMostBlock(block) const parent = this.getParent(block) @@ -85,10 +86,10 @@ const backspaceCtrl = ContentState => { if (start.key !== end.key) { this.removeBlocks(startBlock, endBlock) } - let newBlock = this.getNextSibling(startBlock) + let newBlock = this.findNextBlockInLocation(startBlock) if (!newBlock) { - this.blocks = [this.createBlock()] - newBlock = this.blocks[0] + this.blocks = [this.createBlockP()] + newBlock = this.blocks[0].children[0] } const key = newBlock.key const offset = 0 @@ -121,11 +122,18 @@ const backspaceCtrl = ContentState => { delete startBlock.pos this.codeBlocks.delete(key) } - startBlock.type = 'p' + if (startBlock.type !== 'span') { + startBlock.type = 'span' + const pBlock = this.createBlock('p') + this.insertBefore(pBlock, startBlock) + this.removeBlock(startBlock) + this.appendChild(pBlock, startBlock) + } } startBlock.text = startRemainText + endRemainText this.removeBlocks(startBlock, endBlock) + this.cursor = { start: { key, offset }, end: { key, offset } @@ -136,30 +144,13 @@ const backspaceCtrl = ContentState => { const node = selection.getSelectionStart() const paragraph = findNearestParagraph(node) const id = paragraph.id - const block = this.getBlock(id) + let block = this.getBlock(id) + if (block.type === 'span') block = this.getParent(block) const parent = this.getBlock(block.parent) const preBlock = this.findPreBlockInLocation(block) const { left } = selection.getCaretOffsets(paragraph) const inlineDegrade = this.checkBackspaceCase() - const getPreRow = row => { - const preSibling = this.getBlock(row.preSibling) - if (preSibling) { - return preSibling - } else { - const rowParent = this.getBlock(row.parent) - if (rowParent.type === 'tbody') { - const tHead = this.getBlock(rowParent.preSibling) - return tHead.children[0] - } else { - const table = this.getBlock(rowParent.parent) - const figure = this.getBlock(table.parent) - const figurePreSibling = this.getBlock(figure.preSibling) - return figurePreSibling ? this.lastInDescendant(figurePreSibling) : false - } - } - } - const tableHasContent = table => { const tHead = table.children[0] const tBody = table.children[1] @@ -175,12 +166,11 @@ const backspaceCtrl = ContentState => { const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block event.preventDefault() const value = cm.getValue() - const newBlock = this.createBlock('p') - if (value) newBlock.text = value + const newBlock = this.createBlockP(value) this.insertBefore(newBlock, anchorBlock) this.removeBlock(anchorBlock) this.codeBlocks.delete(id) - const key = newBlock.key + const key = newBlock.children[0].key const offset = 0 this.cursor = { @@ -192,47 +182,27 @@ const backspaceCtrl = ContentState => { } } else if (left === 0 && /th|td/.test(block.type)) { event.preventDefault() + const tHead = this.getBlock(parent.parent) + const table = this.getBlock(tHead.parent) + const figure = this.getBlock(table.parent) + const hasContent = tableHasContent(table) let key let offset - if (preBlock) { + + if ((!preBlock || !/th|td/.test(preBlock.type)) && !hasContent) { + const newLine = this.createBlock('span') + delete figure.functionType + figure.children = [] + this.appendChild(figure, newLine) + figure.text = '' + figure.type = 'p' + key = newLine.key + offset = 0 + } else if (preBlock) { key = preBlock.key offset = preBlock.text.length - } else if (parent) { - const preRow = getPreRow(parent) - const tHead = this.getBlock(parent.parent) - const table = this.getBlock(tHead.parent) - const figure = this.getBlock(table.parent) - const hasContent = tableHasContent(table) - - if (preRow) { - if (preRow.type === 'tr') { - const lastCell = preRow.children[preRow.children.length - 1] - key = lastCell.key - offset = lastCell.text.length - } else { - // if the table is empty change the table to a `p` paragraph - // else set the cursor to the pre block - if (!hasContent) { - figure.children = [] - figure.text = '' - figure.type = 'p' - key = figure.key - offset = 0 - } else { - key = preRow.key - offset = preRow.text.length - } - } - } else { - if (!hasContent) { - figure.children = [] - figure.text = '' - figure.type = 'p' - key = figure.key - offset = 0 - } - } } + if (key !== undefined && offset !== undefined) { this.cursor = { start: { key, offset }, @@ -310,7 +280,9 @@ const backspaceCtrl = ContentState => { preBlock.pos = { line, ch: ch - text.length } this.removeBlock(block) - + if (block.type === 'span' && this.isOnlyChild(block)) { + this.removeBlock(parent) + } this.cursor = { start: { key, offset }, end: { key, offset } @@ -318,6 +290,9 @@ const backspaceCtrl = ContentState => { this.render() } else if (left === 0) { this.removeBlock(block) + if (block.type === 'span' && this.isOnlyChild(block)) { + this.removeBlock(parent) + } } } } diff --git a/src/editor/contentState/codeBlockCtrl.js b/src/editor/contentState/codeBlockCtrl.js index 23a0623b..d7e3c1ed 100644 --- a/src/editor/contentState/codeBlockCtrl.js +++ b/src/editor/contentState/codeBlockCtrl.js @@ -46,13 +46,28 @@ const codeBlockCtrl = ContentState => { * [codeBlockUpdate if block updated to `pre` return true, else return false] */ ContentState.prototype.codeBlockUpdate = function (block) { - const match = CODE_UPDATE_REP.exec(block.text) + if (block.type === 'span') { + block = this.getParent(block) + } + // if it's not a p block, no need to update + if (block.type !== 'p') return false + // if p block's children are more than one, no need to update + if (block.children.length !== 1) return false + const { text } = block.children[0] + const match = CODE_UPDATE_REP.exec(text) if (match) { block.type = 'pre' block.functionType = 'code' block.text = '' block.history = null block.lang = match[1] + block.children = [] + const key = block.key + const offset = 0 + this.cursor = { + start: { key, offset }, + end: { key, offset } + } } return !!match } diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index e977a5a6..7986d3d4 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -1,6 +1,28 @@ import selection from '../selection' const enterCtrl = ContentState => { + ContentState.prototype.chopBlockByCursor = function (block, key, offset) { + const newBlock = this.createBlock('p') + const children = block.children + const index = children.findIndex(child => child.key === key) + const activeLine = this.getBlock(key) + const text = activeLine.text + newBlock.children = children.splice(index + 1) + children[index].nextSibling = null + if (newBlock.children.length) { + newBlock.children[0].preSibling = null + } + if (offset === 0) { + this.removeBlock(activeLine, children) + this.prependChild(newBlock, activeLine) + } else if (offset < text.length) { + activeLine.text = text.substring(0, offset) + const newLine = this.createBlock('span', text.substring(offset)) + this.prependChild(newBlock, newLine) + } + return newBlock + } + ContentState.prototype.chopBlock = function (block) { const parent = this.getParent(block) const type = parent.type @@ -28,20 +50,25 @@ const enterCtrl = ContentState => { return trBlock } - ContentState.prototype.createBlockLi = function (text = '', type = 'p') { + ContentState.prototype.createBlockLi = function (paragraphInListItem) { const liBlock = this.createBlock('li') - const pBlock = this.createBlock(type, text) - this.appendChild(liBlock, pBlock) + if (!paragraphInListItem) { + paragraphInListItem = this.createBlockP() + } + this.appendChild(liBlock, paragraphInListItem) return liBlock } - ContentState.prototype.createTaskItemBlock = function (text = '', checked = false) { + ContentState.prototype.createTaskItemBlock = function (paragraphInListItem, checked = false) { const listItem = this.createBlock('li') - const paragraphInListItem = this.createBlock('p', text) const checkboxInListItem = this.createBlock('input') listItem.listItemType = 'task' checkboxInListItem.checked = checked + + if (!paragraphInListItem) { + paragraphInListItem = this.createBlockP() + } this.appendChild(listItem, checkboxInListItem) this.appendChild(listItem, paragraphInListItem) return listItem @@ -49,53 +76,13 @@ const enterCtrl = ContentState => { ContentState.prototype.enterHandler = function (event) { const { start, end } = selection.getCursorRange() - - if (start.key !== end.key) { - event.preventDefault() - const startBlock = this.getBlock(start.key) - const endBlock = this.getBlock(end.key) - const key = start.key - const offset = start.offset - - const startRemainText = startBlock.type === 'pre' - ? startBlock.text.substring(0, start.offset - 1) - : startBlock.text.substring(0, start.offset) - - const endRemainText = endBlock.type === 'pre' - ? endBlock.text.substring(end.offset - 1) - : endBlock.text.substring(end.offset) - - startBlock.text = startRemainText + endRemainText - - this.removeBlocks(startBlock, endBlock) - this.cursor = { - start: { key, offset }, - end: { key, offset } - } - this.render() - return this.enterHandler(event) - } - - if (start.key === end.key && start.offset !== end.offset) { - event.preventDefault() - const key = start.key - const offset = start.offset - const block = this.getBlock(key) - block.text = block.text.substring(0, start.offset) + block.text.substring(end.offset) - this.cursor = { - start: { key, offset }, - end: { key, offset } - } - this.render() - return this.enterHandler(event) - } - - let paragraph = document.querySelector(`#${start.key}`) let block = this.getBlock(start.key) + const endBlock = this.getBlock(end.key) let parent = this.getParent(block) - // handle float box - const { list, index, show } = this.floatBox const { floatBox } = this + const { list, index, show } = floatBox + + // handle float box if (show) { event.preventDefault() floatBox.cb(list[index]) @@ -107,26 +94,79 @@ const enterCtrl = ContentState => { if (block.type === 'pre') { return } + event.preventDefault() - const getNextBlock = row => { - let nextSibling = this.getBlock(row.nextSibling) - 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 (figure.nextSibling) { - nextSibling = this.getBlock(figure.nextSibling) - } else { - nextSibling = this.createBlock('p') - this.insertAfter(nextSibling, figure) - } + // handle select multiple blocks + if (start.key !== end.key) { + const key = start.key + const offset = start.offset + + const startRemainText = block.type === 'pre' + ? block.text.substring(0, start.offset - 1) + : block.text.substring(0, start.offset) + + const endRemainText = endBlock.type === 'pre' + ? endBlock.text.substring(end.offset - 1) + : endBlock.text.substring(end.offset) + + block.text = startRemainText + endRemainText + + this.removeBlocks(block, endBlock) + this.cursor = { + start: { key, offset }, + end: { key, offset } } - return this.firstInDescendant(nextSibling) + this.render() + return this.enterHandler(event) } - // enter in table + + // handle select multiple charactors + if (start.key === end.key && start.offset !== end.offset) { + const key = start.key + const offset = start.offset + block.text = block.text.substring(0, start.offset) + block.text.substring(end.offset) + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + this.render() + return this.enterHandler(event) + } + + // handle `shift + enter` insert `soft line break` or `hard line break` + // only cursor in `line block` can create `soft line break` and `hard line break` + if (event.shiftKey && block.type === 'span') { + const { text } = block + const newLineText = text.substring(start.offset) + block.text = text.substring(0, start.offset) + const newLine = this.createBlock('span', newLineText) + this.insertAfter(newLine, block) + const { key } = newLine + const offset = 0 + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + return this.render() + } + + // Insert `
` in table cell if you want to open a new line. + // Why not use `soft line break` or `hard line break` ? + // Becasuse table cell only have one line. + if (event.shiftKey && /th|td/.test(block.type)) { + const { text, key } = block + const brTag = '
' + block.text = text.substring(0, start.offset) + brTag + text.substring(start.offset) + const offset = start.offset + brTag.length + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + return this.render() + } + + // handle enter in table if (/th|td/.test(block.type)) { const row = this.getBlock(block.parent) const rowContainer = this.getBlock(row.parent) @@ -143,8 +183,14 @@ const enterCtrl = ContentState => { table.row++ } - const nextSibling = getNextBlock(row) - const key = nextSibling.key + let nextBlock = this.findNextBlockInLocation(block) + // if table(figure block) is the last block, create a new P block after table(figure block). + if (!nextBlock) { + const newBlock = this.createBlockP() + this.insertAfter(newBlock, this.getParent(table)) + nextBlock = newBlock.children[0] + } + const key = nextBlock.key const offset = 0 this.cursor = { @@ -153,6 +199,12 @@ const enterCtrl = ContentState => { } return this.render() } + + if (block.type === 'span') { + block = parent + parent = this.getParent(block) + } + const paragraph = document.querySelector(`#${block.key}`) if ( (parent && parent.type === 'li' && this.isOnlyChild(block)) || (parent && parent.type === 'li' && parent.listItemType === 'task' && parent.children.length === 2) // one `input` and one `p` @@ -171,32 +223,31 @@ const enterCtrl = ContentState => { type = preType let { pre, post } = selection.chopHtmlByCursor(paragraph) - if (/^h\d/.test(type)) { + if (/^h\d$/.test(block.type)) { const PREFIX = /^#+/.exec(pre)[0] post = `${PREFIX} ${post}` - } - - if (type === 'li') { + block.text = pre + newBlock = this.createBlock(type, post) + } else if (block.type === 'p') { + newBlock = this.chopBlockByCursor(block, start.key, start.offset) + } else if (type === 'li') { // handle task item 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) + newBlock = this.chopBlockByCursor(block.children[1], start.key, start.offset) + newBlock = this.createTaskItemBlock(newBlock, checked) } else { - block.children[0].text = pre - newBlock = this.createBlockLi(post) + newBlock = this.chopBlockByCursor(block.children[0], start.key, start.offset) + newBlock = this.createBlockLi(newBlock) newBlock.listItemType = block.listItemType } newBlock.isLooseListItem = block.isLooseListItem - } else { - block.text = pre - newBlock = this.createBlock(type, post) } this.insertAfter(newBlock, block) break case left === 0 && right === 0: // paragraph is empty if (parent && (parent.type === 'blockquote' || parent.type === 'ul')) { - newBlock = this.createBlock('p') + newBlock = this.createBlockP() if (this.isOnlyChild(block)) { this.insertAfter(newBlock, parent) @@ -214,7 +265,7 @@ const enterCtrl = ContentState => { } else if (parent && parent.type === 'li') { if (parent.listItemType === 'task') { const { checked } = parent.children[0] - newBlock = this.createTaskItemBlock('', checked) + newBlock = this.createTaskItemBlock(null, checked) } else { newBlock = this.createBlockLi() newBlock.listItemType = parent.listItemType @@ -227,7 +278,7 @@ const enterCtrl = ContentState => { this.removeBlock(block) } else { - newBlock = this.createBlock('p') + newBlock = this.createBlockP() if (preType === 'li') { const parent = this.getParent(block) this.insertAfter(newBlock, parent) @@ -242,34 +293,47 @@ const enterCtrl = ContentState => { if (preType === 'li') { if (block.listItemType === 'task') { const { checked } = block.children[0] - newBlock = this.createTaskItemBlock('', checked) + newBlock = this.createTaskItemBlock(null, checked) } else { newBlock = this.createBlockLi() newBlock.listItemType = block.listItemType } newBlock.isLooseListItem = block.isLooseListItem } else { - newBlock = this.createBlock('p') + newBlock = this.createBlockP() } if (left === 0 && right !== 0) { this.insertBefore(newBlock, block) newBlock = block } else { + if (block.type === 'p') { + const lastLine = block.children[block.children.length - 1] + if (block.text.trim() === '') this.removeBlock(lastLine) + } this.insertAfter(newBlock, block) } break default: - newBlock = this.createBlock('p') + newBlock = this.createBlockP() this.insertAfter(newBlock, block) break } - this.codeBlockUpdate(newBlock.type === 'li' ? newBlock.children[0] : newBlock) + const getParagraphBlock = block => { + if (block.type === 'li') { + return block.listItemType === 'task' ? block.children[1] : block.children[0] + } else { + return block + } + } + + this.codeBlockUpdate(getParagraphBlock(newBlock)) // If block is pre block when updated, need to focus it. - const blockNeedFocus = this.codeBlockUpdate(block.type === 'li' ? block.children[0] : block) - let tableNeedFocus = this.tableBlockUpdate(block.type === 'li' ? block.children[0] : block) - let htmlNeedFocus = this.updateHtmlBlock(block.type === 'li' ? block.children[0] : block) + const preParagraphBlock = getParagraphBlock(block) + const blockNeedFocus = this.codeBlockUpdate(preParagraphBlock) + let tableNeedFocus = this.tableBlockUpdate(preParagraphBlock) + let htmlNeedFocus = this.updateHtmlBlock(preParagraphBlock) let cursorBlock switch (true) { @@ -288,16 +352,8 @@ const enterCtrl = ContentState => { } let key - if (cursorBlock.type === 'li') { - if (cursorBlock.listItemType === 'task') { - key = cursorBlock.children[1].key - } else { - key = cursorBlock.children[0].key - } - } else { - key = cursorBlock.key - } - + cursorBlock = getParagraphBlock(cursorBlock) + key = cursorBlock.type === 'p' ? cursorBlock.children[0].key : cursorBlock.key const offset = 0 this.cursor = { start: { key, offset }, diff --git a/src/editor/contentState/htmlBlock.js b/src/editor/contentState/htmlBlock.js index 6ef76f9e..2e8550b7 100644 --- a/src/editor/contentState/htmlBlock.js +++ b/src/editor/contentState/htmlBlock.js @@ -91,7 +91,7 @@ const htmlBlock = ContentState => { ContentState.prototype.initHtmlBlock = function (block, tagName) { const isVoidTag = VOID_HTML_TAGS.indexOf(tagName) > -1 - const { text } = block + const { text } = block.children[0] const htmlContent = isVoidTag ? text : `${text}\n\n` const pos = { @@ -110,8 +110,9 @@ const htmlBlock = ContentState => { } ContentState.prototype.updateHtmlBlock = function (block) { - const { type, text } = block + const { type } = block if (type !== 'li' && type !== 'p') return false + const { text } = block.children[0] const match = HTML_BLOCK_REG.exec(text) const tagName = match && match[1] && HTML_TAGS.find(t => t === match[1]) return VOID_HTML_TAGS.indexOf(tagName) === -1 && tagName ? this.initHtmlBlock(block, tagName) : false diff --git a/src/editor/contentState/index.js b/src/editor/contentState/index.js index 6e76b411..c8969b0f 100644 --- a/src/editor/contentState/index.js +++ b/src/editor/contentState/index.js @@ -54,7 +54,7 @@ const convertBlocksToArray = blocks => { return result } -// use to cache the keys which you dont want to remove. +// use to cache the keys which you don't want to remove. const exemption = new Set() class ContentState { @@ -62,7 +62,7 @@ class ContentState { const { eventCenter } = options Object.assign(this, options) this.keys = new Set() - this.blocks = [ this.createBlock() ] + this.blocks = [ this.createBlockP() ] this.stateRender = new StateRender(eventCenter) this.codeBlocks = new Map() this.loadMathMap = new Map() @@ -72,20 +72,16 @@ class ContentState { init () { const lastBlock = this.getLastBlock() + const { key, text } = lastBlock + const offset = text.length this.searchMatches = { - value: '', - matches: [], - index: -1 + value: '', // the search value + matches: [], // matches + index: -1 // active match } this.cursor = { - start: { - key: lastBlock.key, - offset: lastBlock.text.length - }, - end: { - key: lastBlock.key, - offset: lastBlock.text.length - } + start: { key, offset }, + end: { key, offset } } this.history.push({ type: 'normal', @@ -95,8 +91,7 @@ class ContentState { } setCursor () { - const { cursor } = this - selection.setCursorRange(cursor) + selection.setCursorRange(this.cursor) } render (isRenderCursor = true) { @@ -111,7 +106,11 @@ class ContentState { this.renderMath() } - createBlock (type = 'p', text = '') { + /** + * A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph. + * a line block must in a `p block` and `p block`'s children must be line blocks. + */ + createBlock (type = 'span', text = '') { // span type means it is a line block. const key = getUniqueId(this.keys) return { key, @@ -124,11 +123,21 @@ class ContentState { } } + createBlockP (text = '') { + const pBlock = this.createBlock('p') + const lineBlock = this.createBlock('span', text) + this.appendChild(pBlock, lineBlock) + return pBlock + } + + isCollapse (cursor = this.cursor) { + const { start, end } = cursor + return start.key === end.key && start.offset === end.offset + } + // getBlocks getBlocks () { - let key - let cm - for ([ key, cm ] of this.codeBlocks.entries()) { + for (const [ key, cm ] of this.codeBlocks.entries()) { const value = cm.getValue() const block = this.getBlock(key) if (block) block.text = value @@ -298,7 +307,7 @@ class ContentState { } } - removeBlock (block) { + removeBlock (block, fromBlocks = this.blocks) { if (block.type === 'pre') { const codeBlockId = block.key if (this.codeBlocks.has(codeBlockId)) { @@ -328,7 +337,7 @@ class ContentState { } } } - remove(this.blocks, block) + remove(fromBlocks, block) } getActiveBlocks () { @@ -392,6 +401,15 @@ class ContentState { return -1 } + prependChild (parent, block) { + if (parent.children.length) { + const firstChild = parent.children[0] + this.insertBefore(block, firstChild) + } else { + this.appendChild(parent, block) + } + } + appendChild (parent, block) { const len = parent.children.length const lastChild = parent.children[len - 1] diff --git a/src/editor/contentState/tableBlockCtrl.js b/src/editor/contentState/tableBlockCtrl.js index 66ad655e..ce5f2d3d 100644 --- a/src/editor/contentState/tableBlockCtrl.js +++ b/src/editor/contentState/tableBlockCtrl.js @@ -35,11 +35,12 @@ const tableBlockCtrl = ContentState => { let figureBlock if (start.key === end.key) { const startBlock = this.getBlock(start.key) + const anchor = startBlock.type === 'span' ? this.getParent(startBlock) : startBlock if (startBlock.text) { figureBlock = this.createBlock('figure') - this.insertAfter(figureBlock, startBlock) + this.insertAfter(figureBlock, anchor) } else { - figureBlock = startBlock + figureBlock = anchor figureBlock.type = 'figure' figureBlock.functionType = 'table' figureBlock.text = '' @@ -58,7 +59,7 @@ const tableBlockCtrl = ContentState => { } ContentState.prototype.initTable = function (block) { - const { text } = block + const { text } = block.children[0] const rowHeader = [] const len = text.length let i @@ -114,10 +115,12 @@ const tableBlockCtrl = ContentState => { break } case 'delete': { + const newLine = this.createBlock('span') figure.children = [] + this.appendChild(figure, newLine) figure.type = 'p' figure.text = '' - const key = figure.key + const key = newLine.key const offset = 0 this.cursor = { start: { key, offset }, @@ -197,8 +200,9 @@ const tableBlockCtrl = ContentState => { } ContentState.prototype.tableBlockUpdate = function (block) { - const { type, text } = block - if (type !== 'li' && type !== 'p') return false + const { type } = block + if (type !== 'p') return false + const { text } = block.children[0] const match = TABLE_BLOCK_REG.exec(text) 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 a1c113f9..a404b51b 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -18,6 +18,18 @@ const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FREGMENTS.join('|'), 'i') let lastCursor = null const updateCtrl = ContentState => { + // handle task list item checkbox click + ContentState.prototype.listItemCheckBoxClick = function (checkbox) { + const { checked, id } = checkbox + const block = this.getBlock(id) + block.checked = checked + this.render() + } + + ContentState.prototype.checkSameLooseType = function (list, isLooseType) { + return list.children[0].isLooseListItem === isLooseType + } + ContentState.prototype.checkNeedRender = function (block) { const { start: cStart, end: cEnd } = this.cursor const startOffset = cStart.offset @@ -39,11 +51,16 @@ const updateCtrl = ContentState => { } ContentState.prototype.checkInlineUpdate = function (block) { + // table cell can not have blocks in it if (/th|td|figure/.test(block.type)) return false - const { text } = block + // only first line block can update to other block + if (block.type === 'span' && block.preSibling) return false + if (block.type === 'span') { + block = this.getParent(block) + } const parent = this.getParent(block) + const text = block.type === 'p' ? block.children.map(child => child.text).join('\n') : block.text const [match, bullet, tasklist, order, header, blockquote, hr] = text.match(INLINE_UPDATE_REG) || [] - let newType switch (true) { case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1): @@ -54,7 +71,8 @@ const updateCtrl = ContentState => { this.updateList(block, 'bullet', bullet) return true - case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item + // only `bullet` list item can be update to `task` list item + case !!tasklist && parent && parent.listItemType === 'bullet': this.updateTaskListItem(block, 'tasklist', tasklist) return true @@ -63,12 +81,8 @@ const updateCtrl = ContentState => { return true case !!header: - newType = `h${header.length}` - if (block.type !== newType) { - block.type = newType // updateHeader - return true - } - break + this.updateHeader(block, header, text) + return true case !!blockquote: this.updateBlockQuote(block) @@ -76,15 +90,79 @@ const updateCtrl = ContentState => { case !match: default: - newType = 'p' - if (block.type !== newType) { - block.type = newType // updateP - return true - } - break + return this.updateToParagraph(block) + } + } + + // thematic break + ContentState.prototype.updateHr = function (block, marker) { + block.type = 'hr' + block.text = marker + block.children.length = 0 + const { key } = block + this.cursor.start.key = this.cursor.end.key = key + } + + ContentState.prototype.updateList = function (block, type, marker = '') { + const { preferLooseListItem } = this + const parent = this.getParent(block) + const preSibling = this.getPreSibling(block) + const nextSibling = this.getNextSibling(block) + const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol` + const { start, end } = this.cursor + const startOffset = start.offset + const endOffset = end.offset + const newBlock = this.createBlock('li') + block.children[0].text = block.children[0].text.substring(marker.length) + newBlock.listItemType = type + newBlock.isLooseListItem = preferLooseListItem + + if ( + preSibling && preSibling.listType === type && this.checkSameLooseType(preSibling, preferLooseListItem) && + nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem) + ) { + 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 && this.checkSameLooseType(preSibling, preferLooseListItem)) { + this.appendChild(preSibling, newBlock) + + this.removeBlock(block) + } else if (nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem)) { + this.insertBefore(newBlock, nextSibling.children[0]) + + this.removeBlock(block) + } else if (parent && parent.listType === type && this.checkSameLooseType(parent, preferLooseListItem)) { + this.insertBefore(newBlock, block) + + this.removeBlock(block) + } else { + const listBlock = this.createBlock(wrapperTag) + listBlock.listType = type + if (wrapperTag === 'ol') { + const start = marker.split('.')[0] + listBlock.start = /^\d+$/.test(start) ? start : 1 + } + this.appendChild(listBlock, newBlock) + this.insertBefore(listBlock, block) + this.removeBlock(block) } - return false + this.appendChild(newBlock, block) + + const key = block.children[0].key + this.cursor = { + start: { + key, + offset: Math.max(0, startOffset - marker.length) + }, + end: { + key, + offset: Math.max(0, endOffset - marker.length) + } + } } ContentState.prototype.updateTaskListItem = function (block, type, marker = '') { @@ -97,7 +175,7 @@ const updateCtrl = ContentState => { checkbox.checked = checked this.insertBefore(checkbox, block) - block.text = block.text.substring(marker.length) + block.children[0].text = block.children[0].text.substring(marker.length) parent.listItemType = 'task' parent.isLooseListItem = preferLooseListItem @@ -146,102 +224,47 @@ const updateCtrl = ContentState => { } } - // handle task list item checkbox click - ContentState.prototype.listItemCheckBoxClick = function (checkbox) { - const { checked, id } = checkbox - const block = this.getBlock(id) - block.checked = checked - this.render() - } - - ContentState.prototype.checkSameLooseType = function (list, isLooseType) { - return list.children[0].isLooseListItem === isLooseType - } - - ContentState.prototype.updateList = function (block, type, marker = '') { - const { preferLooseListItem } = this - const parent = this.getParent(block) - const preSibling = this.getPreSibling(block) - const nextSibling = this.getNextSibling(block) - 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 - const newBlock = this.createBlockLi(newText, block.type) - newBlock.listItemType = type - newBlock.isLooseListItem = preferLooseListItem - - if ( - preSibling && preSibling.listType === type && this.checkSameLooseType(preSibling, preferLooseListItem) && - nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem) - ) { - 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 && this.checkSameLooseType(preSibling, preferLooseListItem)) { - this.appendChild(preSibling, newBlock) - - this.removeBlock(block) - } else if (nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem)) { - this.insertBefore(newBlock, nextSibling.children[0]) - - this.removeBlock(block) - } else if (parent && parent.listType === type && this.checkSameLooseType(parent, preferLooseListItem)) { - 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 = /^\d+$/.test(start) ? start : 1 - } - this.appendChild(block, newBlock) + ContentState.prototype.updateHeader = function (block, header, text) { + const newType = `h${header.length}` + if (block.type !== newType) { + block.type = newType + block.text = text + block.children.length = 0 } - - const key = newBlock.type === 'li' ? newBlock.children[0].key : newBlock.key - this.cursor = { - start: { - key, - offset: Math.max(0, startOffset - marker.length) - }, - end: { - key, - offset: Math.max(0, endOffset - marker.length) - } - } - return newBlock.children[0] + this.cursor.start.key = this.cursor.end.key = block.key } ContentState.prototype.updateBlockQuote = function (block) { - const newText = block.text.substring(1).trim() - const newPblock = this.createBlock('p', newText) - block.type = 'blockquote' - block.text = '' - this.appendChild(block, newPblock) + block.children[0].text = block.children[0].text.substring(1).trim() + const quoteBlock = this.createBlock('blockquote') + this.insertBefore(quoteBlock, block) + this.removeBlock(block) + this.appendChild(quoteBlock, block) const { start, end } = this.cursor - const key = newPblock.key this.cursor = { start: { - key, + key: start.key, offset: start.offset - 1 }, end: { - key, + key: end.key, offset: end.offset - 1 } } } - // thematic break - ContentState.prototype.updateHr = function (block, marker) { - block.type = 'hr' + ContentState.prototype.updateToParagraph = function (block) { + const newType = 'p' + if (block.type !== newType) { + block.type = newType // updateP + const newLine = this.createBlock('span', block.text) + this.appendChild(block, newLine) + block.text = '' + this.cursor.start.key = this.cursor.end.key = newLine.key + return true + } + return false } ContentState.prototype.updateState = function (event) { @@ -321,9 +344,12 @@ const updateCtrl = ContentState => { } // remove temp block which generated by operation on code block if (block && block.key !== oldKey) { - const oldBlock = this.getBlock(oldKey) + let oldBlock = this.getBlock(oldKey) if (oldBlock) this.codeBlockUpdate(oldBlock) - if (oldBlock && oldBlock.temp) { + if (oldBlock && oldBlock.type === 'span') { + oldBlock = this.getParent(oldBlock) + } + if (oldBlock && oldBlock.temp && oldBlock.type === 'p') { if (oldBlock.text || oldBlock.children.length) { delete oldBlock.temp } else { @@ -380,9 +406,8 @@ const updateCtrl = ContentState => { } this.cursor = lastCursor = { start, end } - const checkMarkedUpdate = this.checkNeedRender(block) - const checkInlineUpdate = this.checkInlineUpdate(block) + const checkInlineUpdate = this.isCollapse() && this.checkInlineUpdate(block) if (checkMarkedUpdate || checkInlineUpdate || needRender) { this.render() diff --git a/src/editor/index.css b/src/editor/index.css index ea739f86..4ba8f3cc 100644 --- a/src/editor/index.css +++ b/src/editor/index.css @@ -33,6 +33,20 @@ h6.ag-active::before { font-weight: 100; } +.ag-paragraph:empty::after, +.ag-line:empty:after { + content: '\200B' +} + +.ag-line { + display: block; +} + +.ag-hard-line-break::after { + content: '↓'; + opacity: .3; +} + *::selection, .ag-selection { background: #E4E7ED; color: #303133; diff --git a/src/editor/index.js b/src/editor/index.js index 7c6e49d7..6cf04b71 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -46,7 +46,6 @@ class Aganippe { this.ensureContainerDiv() const { container, contentState, eventCenter } = this contentState.stateRender.setContainer(container.children[0]) - contentState.render() eventCenter.subscribe('editEmoji', throttle(this.subscribeEditEmoji.bind(this), 200)) this.dispatchEditEmoji() diff --git a/src/editor/parser/StateRender.js b/src/editor/parser/StateRender.js index 70bc33f8..8ed1f958 100644 --- a/src/editor/parser/StateRender.js +++ b/src/editor/parser/StateRender.js @@ -62,9 +62,13 @@ class StateRender { const type = block.type === 'hr' ? 'p' : block.type 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']}` - : `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}` + let blockSelector = `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}` + if (isActive) { + blockSelector += `.${CLASS_OR_ID['AG_ACTIVE']}` + } + if (type === 'span') { + blockSelector += `.${CLASS_OR_ID['AG_LINE']}` + } const data = { attrs: {}, @@ -134,17 +138,15 @@ class StateRender { if (block.type === 'ol') { Object.assign(data.attrs, { start: block.start }) } - return h(blockSelector, data, block.children.map(child => renderBlock(child))) } else { // highlight search key in block const highlights = matches.filter(m => m.key === block.key) - let children = block.text - ? tokenizer(block.text, highlights).reduce((acc, token) => { - const chunk = this[token.type](h, cursor, block, token) - return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] - }, []) - : [ h(LOWERCASE_TAGS.br) ] + const { text } = block + let children = '' + if (text) { + children = tokenizer(text, highlights).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], []) + } if (/th|td/.test(block.type)) { const { align } = block @@ -207,8 +209,8 @@ class StateRender { }) const newVdom = h(selector, children) - const root = document.querySelector(selector) || this.container - const oldVdom = toVNode(root) + const rootDom = document.querySelector(selector) || this.container + const oldVdom = toVNode(rootDom) patch(oldVdom, newVdom) } @@ -233,13 +235,25 @@ class StateRender { ['tail_header'] (h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) const { start, end } = token.range + const content = this.highlight(h, block, start, end, token) if (/^h\d$/.test(block.type)) { - const content = this.highlight(h, block, start, end, token) return [ h(`span.${className}`, content) ] } else { - return this.highlight(h, block, start, end, token) + return content + } + } + + ['hard_line_break'] (h, cursor, block, token, outerClass) { + const className = CLASS_OR_ID['AG_HARD_LINE_BREAK'] + const content = [ token.spaces ] + if (block.type === 'span' && block.nextSibling) { + return [ + h(`span.${className}`, content) + ] + } else { + return content } } @@ -309,6 +323,7 @@ class StateRender { h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, endMarker) ] } + // change text to highlight vdom highlight (h, block, rStart, rEnd, token) { const { text } = block @@ -668,8 +683,9 @@ class StateRender { const className = CLASS_OR_ID['AG_HTML_TAG'] const { start, end } = token.range const tag = this.highlight(h, block, start, end, token) + const isBr = /)/.test(token.tag) return [ - h(`span.${className}`, tag) + h(`span.${className}`, isBr ? [...tag, h('br')] : tag) ] } diff --git a/src/editor/parser/parse.js b/src/editor/parser/parse.js index 4f987671..38399fb2 100644 --- a/src/editor/parser/parse.js +++ b/src/editor/parser/parse.js @@ -101,7 +101,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top) => { pending = '' } - if (beginRules) { + if (beginRules && pos === 0) { const beginR = ['header', 'hr', 'code_fense', 'display_math'] for (const ruleName of beginR) { @@ -350,6 +350,25 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top) => { pos = pos + autoLTo[0].length continue } + // hard line break + const hardTo = inlineRules['hard_line_break'].exec(src) + if (hardTo && top) { + const len = hardTo[0].length + pushPending() + tokens.push({ + type: 'hard_line_break', + spaces: hardTo[1], + parent: tokens, + range: { + start: pos, + end: pos + len + } + }) + src = src.substring(len) + pos += len + continue + } + // tail header const tailTo = inlineRules['tail_header'].exec(src) if (tailTo && top) { @@ -442,13 +461,16 @@ export const generator = tokens => { case 'auto_link': result += token.href break - case 'html-image': + case 'html_image': case 'html_tag': result += token.tag break case 'tail_header': result += token.marker break + case 'hard_line_break': + result += token.spaces + break default: throw new Error(`unhandle token type: ${token.type}`) } diff --git a/src/editor/parser/rules.js b/src/editor/parser/rules.js index d0792355..4fc06ad6 100644 --- a/src/editor/parser/rules.js +++ b/src/editor/parser/rules.js @@ -20,6 +20,7 @@ export const inlineRules = { 'tail_header': /^(\s{1,}#{1,})(\s*)$/, 'a_link': /^()[\s\S]*(?!\\)>)([\s\S]*)(<\/a>)/, // can nest 'html_image': /^()/, - 'html_tag': /^(|<\/?[a-zA-Z\d-]+[\s\S]*?(?!\\)>)/ + 'html_tag': /^(|<\/?[a-zA-Z\d-]+[\s\S]*?(?!\\)>)/, + 'hard_line_break': /^(\s{2,})$/ } /* eslint-enable no-useless-escape */ diff --git a/src/editor/selection.js b/src/editor/selection.js index 9618c0fe..481b8db4 100644 --- a/src/editor/selection.js +++ b/src/editor/selection.js @@ -720,7 +720,6 @@ class Selection { const { start, end } = cursorRange const startParagraph = document.querySelector(`#${start.key}`) const endParagraph = document.querySelector(`#${end.key}`) - const getNodeAndOffset = (node, offset) => { if (node.nodeType === 3) { return { diff --git a/src/editor/utils/exportMarkdown.js b/src/editor/utils/exportMarkdown.js index 92cceb9c..430b0ef2 100644 --- a/src/editor/utils/exportMarkdown.js +++ b/src/editor/utils/exportMarkdown.js @@ -24,6 +24,12 @@ class ExportMarkdown { for (const block of blocks) { switch (block.type) { case 'p': + this.insertLineBreak(result, indent, true) + result.push(this.translateBlocks2Markdown(block.children, indent)) + break + case 'span': + result.push(this.normalizeParagraphText(block, indent)) + break case 'hr': this.insertLineBreak(result, indent, true) result.push(this.normalizeParagraphText(block, indent)) diff --git a/src/editor/utils/exportStyledHTML.js b/src/editor/utils/exportStyledHTML.js index 12b4568c..5111b3fb 100644 --- a/src/editor/utils/exportStyledHTML.js +++ b/src/editor/utils/exportStyledHTML.js @@ -154,6 +154,8 @@ class ExportHTML { $(removeClassNames.join(', ')).remove() $(`.${CLASS_OR_ID['AG_ACTIVE']}`).removeClass(CLASS_OR_ID['AG_ACTIVE']) $(`[data-role=hr]`).replaceWith('
') + + // replace the `emoji text` with actual emoji const emojis = $(`span.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`) if (emojis.length > 0) { emojis.each((i, e) => { @@ -173,9 +175,9 @@ class ExportHTML { }) } - // change `data-href` to `href` attribute + // change `data-href` to `href` attribute, so the anchor can be clicked. const anchors = $(`a[data-href]`) - if (anchors.length > 0) { + if (anchors.length) { anchors.each((i, a) => { const anchor = $(a) const href = anchor.attr('data-href') @@ -184,10 +186,32 @@ class ExportHTML { anchor.attr('target', '_blank') }) } + // soft line break render to html is a space, and hard line break render to html is `
` + const paragraphs = $(`p.${CLASS_OR_ID['AG_PARAGRAPH']}`) + if (paragraphs.length) { + paragraphs.each((i, p) => { + const paragraph = $(p) + const children = paragraph.children() + const len = children.length + children.each((i, c) => { + const child = $(c) + child.removeClass(CLASS_OR_ID['AG_LINE']) + if (i < len - 1) { // no need to handle the last line + const hardLineBreak = $(`.${CLASS_OR_ID['AG_HARD_LINE_BREAK']}`, child) + if (hardLineBreak.length) { + hardLineBreak.removeClass(CLASS_OR_ID['AG_HARD_LINE_BREAK']) + hardLineBreak.append('
') + } else { + $(' ').appendTo(child) + } + } + }) + }) + } + return $('body').html() .replace(/([\s\S]+?)<\/span>/g, (m, p1) => { - if (/script|style|title/.test(p1)) return p1 - else return unescapeHtml(p1) + return /script|style|title/.test(p1) ? p1 : unescapeHtml(p1) }) } } diff --git a/src/editor/utils/importMarkdown.js b/src/editor/utils/importMarkdown.js index c9c5248e..76b7de17 100644 --- a/src/editor/utils/importMarkdown.js +++ b/src/editor/utils/importMarkdown.js @@ -13,6 +13,8 @@ import { turndownConfig, CLASS_OR_ID, CURSOR_DNA, TABLE_TOOLS, BLOCK_TYPE7 } fro const turndownPluginGfm = require('turndown-plugin-gfm') +const LINE_BREAKS = /\n/ + // turn html to markdown const turndownService = new TurndownService(turndownConfig) const gfm = turndownPluginGfm.gfm @@ -103,7 +105,6 @@ const importRegister = ContentState => { switch (child.nodeName) { case 'th': case 'td': - case 'p': case 'h1': case 'h2': case 'h3': @@ -111,10 +112,6 @@ const importRegister = ContentState => { case 'h5': case 'h6': const textValue = child.childNodes.length ? child.childNodes[0].value : '' - if (checkIsHTML(textValue) && child.nodeName === 'p') { - travel(parent, child.childNodes) - break - } const match = /\d/.exec(child.nodeName) value = match ? '#'.repeat(+match[0]) + ` ${textValue}` : textValue block = this.createBlock(child.nodeName, value) @@ -134,6 +131,17 @@ const importRegister = ContentState => { this.appendChild(parent, block) break + case 'p': + value = child.childNodes.length ? child.childNodes[0].value : '' + if (checkIsHTML(value)) { + travel(parent, child.childNodes) + } else { + block = this.createBlock('p') + travel(block, child.childNodes) + this.appendChild(parent, block) + } + break + case 'table': const toolBar = this.createToolBar(TABLE_TOOLS, 'table') const table = this.createBlock('table') @@ -243,13 +251,23 @@ const importRegister = ContentState => { this.appendChild(parent, block) } else { // not html block - block = this.createBlock('p', fragment) + block = this.createBlockP(fragment) this.appendChild(parent, block) } }) - } else { - block = this.createBlock('p', value.replace(/^\s+/, '')) // fix: #153 + } else if (parentNode.nodeName === 'li') { + block = this.createBlock('p') + // fix: #153 + const lines = value.replace(/^\s+/, '').split(LINE_BREAKS).map(line => this.createBlock('span', line)) + for (const line of lines) { + this.appendChild(block, line) + } this.appendChild(parent, block) + } else if (parentNode.nodeName === 'p') { + const lines = value.split(LINE_BREAKS).map(line => this.createBlock('span', line)) + for (const line of lines) { + this.appendChild(parent, line) + } } } break @@ -264,7 +282,7 @@ const importRegister = ContentState => { } travel(rootState, childNodes) - return rootState.children.length ? rootState.children : [this.createBlock()] + return rootState.children.length ? rootState.children : [this.createBlockP()] } // transform `paste's text/html data` to content state blocks. ContentState.prototype.html2State = function (html) {