mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 05:40:39 +08:00
Line break (#197)
* update: change log * line break * feature: line break, support event and import and export markdown * shift enter in table cell * fix: not create a new paragraph when presss enter in the last cell of end table block * fix: html block can not work * feature: line break export to html * fix: problem2 * fix: problem 4
This commit is contained in:
parent
ce6efbe5f5
commit
cfd0d0a2fa
19
.github/CHANGELOG.md
vendored
19
.github/CHANGELOG.md
vendored
@ -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**
|
||||
|
1
src/editor/assets/icons/enter.svg
Normal file
1
src/editor/assets/icons/enter.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523863913565" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11902" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M900.2 385.8v-192c0-22.1-17.9-40-40-40s-40 17.9-40 40v192c0 59.4-23.1 115.3-65.2 157.4-42 42-97.9 65.2-157.4 65.2H258.2l112.4-112.3c15.6-15.7 15.6-41 0-56.6-7.8-7.8-18-11.7-28.3-11.7s-20.5 3.9-28.3 11.7L135.5 618c-7.5 7.5-11.7 17.7-11.7 28.3s4.2 20.8 11.7 28.3l183.7 183.8c15.7 15.6 41 15.7 56.6 0s15.6-41 0-56.6L262.4 688.4h335.2c80.8 0 156.8-31.4 214-88.6s88.7-133.2 88.6-214z" p-id="11903" fill="#cdcdcd"></path></svg>
|
After Width: | Height: | Size: 808 B |
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 `<br/>` 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 = '<br/>'
|
||||
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 },
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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 = /<br(?=\s|\/|>)/.test(token.tag)
|
||||
return [
|
||||
h(`span.${className}`, tag)
|
||||
h(`span.${className}`, isBr ? [...tag, h('br')] : tag)
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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}`)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export const inlineRules = {
|
||||
'tail_header': /^(\s{1,}#{1,})(\s*)$/,
|
||||
'a_link': /^(<a[\s\S]*href\s*=\s*("|')(.+?)\2(?=\s|>)[\s\S]*(?!\\)>)([\s\S]*)(<\/a>)/, // can nest
|
||||
'html_image': /^(<img\s([\s\S]*?src[\s\S]+?)(?!\\)>)/,
|
||||
'html_tag': /^(<!--[\s\S]*?-->|<\/?[a-zA-Z\d-]+[\s\S]*?(?!\\)>)/
|
||||
'html_tag': /^(<!--[\s\S]*?-->|<\/?[a-zA-Z\d-]+[\s\S]*?(?!\\)>)/,
|
||||
'hard_line_break': /^(\s{2,})$/
|
||||
}
|
||||
/* eslint-enable no-useless-escape */
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -154,6 +154,8 @@ class ExportHTML {
|
||||
$(removeClassNames.join(', ')).remove()
|
||||
$(`.${CLASS_OR_ID['AG_ACTIVE']}`).removeClass(CLASS_OR_ID['AG_ACTIVE'])
|
||||
$(`[data-role=hr]`).replaceWith('<hr>')
|
||||
|
||||
// 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 `<br>`
|
||||
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('<br/>')
|
||||
} else {
|
||||
$('<span> </span>').appendTo(child)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return $('body').html()
|
||||
.replace(/<span class="ag-html-tag">([\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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user