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${tagName}>`
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) {