fix some performance problem, add partial render and undo depth (#222)

* fix some performance problem, add partial render and undo depth

* optimization of performance: rewrite getBlock, getBlocks ...

* optimization of performance: cache the result of tokenization

* optimization of performance: render code block only needed
This commit is contained in:
冉四夕 2018-04-30 01:46:09 +08:00 committed by GitHub
parent d19dc9f4b8
commit f569d2e9d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 255 additions and 159 deletions

2
.github/TODOLIST.md vendored
View File

@ -68,6 +68,8 @@
- [ ] Refactor data structure of contentState, add copyBlock... - [ ] Refactor data structure of contentState, add copyBlock...
- [ ] Refactor the History data structure
##### Documents ##### Documents
- [ ] Manual - [ ] Manual

View File

@ -3,60 +3,7 @@ import { isCursorAtFirstLine, isCursorAtLastLine, isCursorAtBegin, isCursorAtEnd
import { findNearestParagraph } from '../utils/domManipulate' import { findNearestParagraph } from '../utils/domManipulate'
import selection from '../selection' import selection from '../selection'
const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
const arrowCtrl = ContentState => { const arrowCtrl = ContentState => {
ContentState.prototype.firstInDescendant = function (block) {
const children = block.children
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (children.length) {
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
return this.firstInDescendant(children[1])
} else {
return this.firstInDescendant(children[0])
}
}
}
ContentState.prototype.lastInDescendant = function (block) {
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (block.children.length) {
const children = block.children
let lastChild = children[children.length - 1]
while (lastChild.editable === false) {
lastChild = this.getPreSibling(lastChild)
}
return this.lastInDescendant(lastChild)
}
}
ContentState.prototype.findPreBlockInLocation = function (block) {
const parent = this.getParent(block)
const preBlock = this.getPreSibling(block)
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
return this.lastInDescendant(preBlock)
} else if (parent) {
return this.findPreBlockInLocation(parent)
} else {
return null
}
}
ContentState.prototype.findNextBlockInLocation = function (block) {
const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block)
if (block.nextSibling && nextBlock.editable !== false) {
return this.firstInDescendant(nextBlock)
} else if (parent) {
return this.findNextBlockInLocation(parent)
} else {
return null
}
}
ContentState.prototype.findNextRowCell = function (cell) { ContentState.prototype.findNextRowCell = function (cell) {
if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`) if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`)
const row = this.getParent(cell) const row = this.getParent(cell)
@ -126,6 +73,7 @@ const arrowCtrl = ContentState => {
if (show && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown)) { if (show && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown)) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
switch (event.key) { switch (event.key) {
case EVENT_KEYS.ArrowDown: case EVENT_KEYS.ArrowDown:
if (index < list.length - 1) { if (index < list.length - 1) {
@ -147,6 +95,7 @@ const arrowCtrl = ContentState => {
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block
let activeBlock let activeBlock
event.preventDefault() event.preventDefault()
event.stopPropagation()
switch (event.key) { switch (event.key) {
case EVENT_KEYS.ArrowLeft: // fallthrough case EVENT_KEYS.ArrowLeft: // fallthrough
case EVENT_KEYS.ArrowUp: case EVENT_KEYS.ArrowUp:
@ -236,6 +185,7 @@ const arrowCtrl = ContentState => {
if (activeBlock) { if (activeBlock) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
const offset = activeBlock.type === 'p' ? 0 : (event.key === EVENT_KEYS.ArrowUp ? activeBlock.text.length : 0) const offset = activeBlock.type === 'p' ? 0 : (event.key === EVENT_KEYS.ArrowUp ? activeBlock.text.length : 0)
const key = activeBlock.type === 'p' ? activeBlock.children[0].key : activeBlock.key const key = activeBlock.type === 'p' ? activeBlock.children[0].key : activeBlock.key
this.cursor = { this.cursor = {
@ -258,6 +208,7 @@ const arrowCtrl = ContentState => {
(preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowLeft && left === 0) (preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowLeft && left === 0)
) { ) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
const key = preBlock.key const key = preBlock.key
const offset = 0 const offset = 0
this.cursor = { this.cursor = {
@ -274,6 +225,7 @@ const arrowCtrl = ContentState => {
(nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowRight && right === 0) (nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowRight && right === 0)
) { ) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
const key = nextBlock.key const key = nextBlock.key
const offset = 0 const offset = 0
this.cursor = { this.cursor = {
@ -289,6 +241,7 @@ const arrowCtrl = ContentState => {
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0) (event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
) { ) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
if (!preBlock) return if (!preBlock) return
const key = preBlock.key const key = preBlock.key
const offset = preBlock.text.length const offset = preBlock.text.length
@ -302,6 +255,7 @@ const arrowCtrl = ContentState => {
(event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length) (event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length)
) { ) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
let key let key
if (nextBlock) { if (nextBlock) {
key = nextBlock.key key = nextBlock.key

View File

@ -2,7 +2,7 @@ import createDOMPurify from 'dompurify'
import codeMirror, { setMode, setCursorAtLastLine } from '../codeMirror' import codeMirror, { setMode, setCursorAtLastLine } from '../codeMirror'
import { createInputInCodeBlock } from '../utils/domManipulate' import { createInputInCodeBlock } from '../utils/domManipulate'
import { escapeInBlockHtml } from '../utils' import { escapeInBlockHtml } from '../utils'
import { codeMirrorConfig, CLASS_OR_ID, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config' import { codeMirrorConfig, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config'
const DOMPurify = createDOMPurify(window) const DOMPurify = createDOMPurify(window)
@ -72,9 +72,10 @@ const codeBlockCtrl = ContentState => {
return !!match return !!match
} }
ContentState.prototype.pre2CodeMirror = function (isRenderCursor) { ContentState.prototype.pre2CodeMirror = function (isRenderCursor, emptyPres) {
if (!emptyPres.length) return
const { eventCenter } = this const { eventCenter } = this
const selector = `pre.${CLASS_OR_ID['AG_CODE_BLOCK']}, pre.${CLASS_OR_ID['AG_HTML_BLOCK']}` const selector = emptyPres.map(key => `pre#${key}`).join(', ')
const pres = document.querySelectorAll(selector) const pres = document.querySelectorAll(selector)
Array.from(pres).forEach(pre => { Array.from(pres).forEach(pre => {
const id = pre.id const id = pre.id

View File

@ -48,8 +48,13 @@ export class History {
} }
} }
push (state) { push (state) {
const UNDO_DEPTH = 20
this.stack.splice(this.index + 1) this.stack.splice(this.index + 1)
this.stack.push(deepCopy(state)) this.stack.push(deepCopy(state))
if (this.stack.length > UNDO_DEPTH) {
this.stack.shift()
this.index = this.index - 1
}
this.index = this.index + 1 this.index = this.index + 1
} }
} }

View File

@ -1,3 +1,4 @@
import { setCursorAtLastLine } from '../codeMirror'
import { getUniqueId } from '../utils' import { getUniqueId } from '../utils'
import selection from '../selection' import selection from '../selection'
import StateRender from '../parser/StateRender' import StateRender from '../parser/StateRender'
@ -42,17 +43,7 @@ const prototypes = [
importMarkdown importMarkdown
] ]
// deep first search const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
const convertBlocksToArray = blocks => {
const result = []
blocks.forEach(block => {
result.push(block)
if (block.children.length) {
result.push(...convertBlocksToArray(block.children))
}
})
return result
}
// use to cache the keys which you don't want to remove. // use to cache the keys which you don't want to remove.
const exemption = new Set() const exemption = new Set()
@ -91,7 +82,20 @@ class ContentState {
} }
setCursor () { setCursor () {
const { start: { key } } = this.cursor
const block = this.getBlock(key)
if (block.type !== 'pre') {
selection.setCursorRange(this.cursor) selection.setCursorRange(this.cursor)
} else {
const cm = this.codeBlocks.get(key)
const { pos } = block
if (pos) {
cm.focus()
cm.setCursor(pos)
} else {
setCursorAtLastLine(cm)
}
}
} }
render (isRenderCursor = true) { render (isRenderCursor = true) {
@ -100,12 +104,19 @@ class ContentState {
matches.forEach((m, i) => { matches.forEach((m, i) => {
m.active = i === index m.active = i === index
}) })
this.stateRender.render(blocks, cursor, activeBlocks, matches) const emptyPres = this.stateRender.render(blocks, cursor, activeBlocks, matches)
this.pre2CodeMirror(isRenderCursor, emptyPres)
if (isRenderCursor) this.setCursor() if (isRenderCursor) this.setCursor()
this.pre2CodeMirror(isRenderCursor)
this.renderMath() this.renderMath()
} }
partialRender (block) {
const { cursor } = this
this.stateRender.partialRender(block, cursor)
this.setCursor()
this.renderMath(block)
}
/** /**
* A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph. * 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. * a line block must in a `p block` and `p block`'s children must be line blocks.
@ -137,11 +148,11 @@ class ContentState {
// getBlocks // getBlocks
getBlocks () { getBlocks () {
for (const [ key, cm ] of this.codeBlocks.entries()) { // for (const [ key, cm ] of this.codeBlocks.entries()) {
const value = cm.getValue() // const value = cm.getValue()
const block = this.getBlock(key) // const block = this.getBlock(key)
if (block) block.text = value // if (block) block.text = value
} // }
return this.blocks return this.blocks
} }
@ -149,12 +160,22 @@ class ContentState {
return this.cursor return this.cursor
} }
getArrayBlocks () {
return convertBlocksToArray(this.blocks)
}
getBlock (key) { getBlock (key) {
return this.getArrayBlocks().filter(block => block.key === key)[0] let result = null
const travel = blocks => {
for (const block of blocks) {
if (block.key === key) {
result = block
return
}
const { children } = block
if (children.length) {
travel(children)
}
}
}
travel(this.blocks)
return result
} }
getParent (block) { getParent (block) {
@ -183,15 +204,6 @@ class ContentState {
return block.nextSibling ? this.getBlock(block.nextSibling) : null return block.nextSibling ? this.getBlock(block.nextSibling) : null
} }
getFirstBlock () {
const arrayBlocks = this.getArrayBlocks()
if (arrayBlocks.length) {
return arrayBlocks[0]
} else {
throw new Error('article need at least has one paragraph')
}
}
/** /**
* if target is descendant of parent return true, else return false * if target is descendant of parent return true, else return false
* @param {[type]} parent [description] * @param {[type]} parent [description]
@ -459,39 +471,76 @@ class ContentState {
return null return null
} }
getLastBlock () { firstInDescendant (block) {
const arrayBlocks = this.getArrayBlocks() const children = block.children
const len = arrayBlocks.length if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
if (len) { return block
return arrayBlocks[len - 1] } else if (children.length) {
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
return this.firstInDescendant(children[1])
} else { } else {
throw new Error('article need at least has one paragraph') return this.firstInDescendant(children[0])
}
} }
} }
wordCount () { lastInDescendant (block) {
const blocks = this.getBlocks() if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
let paragraph = blocks.length return block
} else if (block.children.length) {
const children = block.children
let lastChild = children[children.length - 1]
while (lastChild.editable === false) {
lastChild = this.getPreSibling(lastChild)
}
return this.lastInDescendant(lastChild)
}
}
findPreBlockInLocation (block) {
const parent = this.getParent(block)
const preBlock = this.getPreSibling(block)
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
return this.lastInDescendant(preBlock)
} else if (parent) {
return this.findPreBlockInLocation(parent)
} else {
return null
}
}
findNextBlockInLocation (block) {
const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block)
if (block.nextSibling && nextBlock.editable !== false) {
return this.firstInDescendant(nextBlock)
} else if (parent) {
return this.findNextBlockInLocation(parent)
} else {
return null
}
}
getLastBlock () {
const { blocks } = this
const len = blocks.length
return this.lastInDescendant(blocks[len - 1])
}
wordCount (markdown) {
let paragraph = this.blocks.length
let word = 0 let word = 0
let character = 0 let character = 0
let all = 0 let all = 0
const travel = block => { const removedChinese = markdown.replace(/[\u4e00-\u9fa5]/g, '')
if (block.text) {
const text = block.text
const removedChinese = text.replace(/[\u4e00-\u9fa5]/g, '')
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t) const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
const chineseWordLength = text.length - removedChinese.length const chineseWordLength = markdown.length - removedChinese.length
word += chineseWordLength + tokens.length word += chineseWordLength + tokens.length
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
all += text.length all += markdown.length
}
if (block.children.length) {
block.children.forEach(child => travel(child))
}
}
blocks.forEach(block => travel(block))
return { word, paragraph, character, all } return { word, paragraph, character, all }
} }

View File

@ -4,8 +4,9 @@ import { CLASS_OR_ID } from '../config'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
const mathCtrl = ContentState => { const mathCtrl = ContentState => {
ContentState.prototype.renderMath = function () { ContentState.prototype.renderMath = function (block) {
const mathEles = document.querySelectorAll(`.${CLASS_OR_ID['AG_MATH_RENDER']}`) const selector = block ? `#${block.key} .${CLASS_OR_ID['AG_MATH_RENDER']}` : `.${CLASS_OR_ID['AG_MATH_RENDER']}`
const mathEles = document.querySelectorAll(selector)
const { loadMathMap } = this const { loadMathMap } = this
for (const math of mathEles) { for (const math of mathEles) {
const content = math.getAttribute('data-math') const content = math.getAttribute('data-math')

View File

@ -1,8 +1,8 @@
import selection from '../selection' import selection from '../selection'
import { tokenizer } from '../parser/parse' import { tokenizer } from '../parser/parse'
import { conflict } from '../utils' import { conflict, isMetaKey } from '../utils'
import { getTextContent } from '../utils/domManipulate' import { getTextContent } from '../utils/domManipulate'
import { CLASS_OR_ID } from '../config' import { CLASS_OR_ID, EVENT_KEYS } from '../config'
const INLINE_UPDATE_FREGMENTS = [ const INLINE_UPDATE_FREGMENTS = [
'^([*+-]\\s)', // Bullet list '^([*+-]\\s)', // Bullet list
@ -64,8 +64,7 @@ const updateCtrl = ContentState => {
switch (true) { switch (true) {
case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1): case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
this.updateHr(block, hr) return this.updateHr(block, hr)
return true
case !!bullet: case !!bullet:
this.updateList(block, 'bullet', bullet) this.updateList(block, 'bullet', bullet)
@ -81,8 +80,7 @@ const updateCtrl = ContentState => {
return true return true
case !!header: case !!header:
this.updateHeader(block, header, text) return this.updateHeader(block, header, text)
return true
case !!blockquote: case !!blockquote:
this.updateBlockQuote(block) this.updateBlockQuote(block)
@ -96,11 +94,15 @@ const updateCtrl = ContentState => {
// thematic break // thematic break
ContentState.prototype.updateHr = function (block, marker) { ContentState.prototype.updateHr = function (block, marker) {
if (block.type !== 'hr') {
block.type = 'hr' block.type = 'hr'
block.text = marker block.text = marker
block.children.length = 0 block.children.length = 0
const { key } = block const { key } = block
this.cursor.start.key = this.cursor.end.key = key this.cursor.start.key = this.cursor.end.key = key
return true
}
return false
} }
ContentState.prototype.updateList = function (block, type, marker = '') { ContentState.prototype.updateList = function (block, type, marker = '') {
@ -230,8 +232,10 @@ const updateCtrl = ContentState => {
block.type = newType block.type = newType
block.text = text block.text = text
block.children.length = 0 block.children.length = 0
}
this.cursor.start.key = this.cursor.end.key = block.key this.cursor.start.key = this.cursor.end.key = block.key
return true
}
return false
} }
ContentState.prototype.updateBlockQuote = function (block) { ContentState.prototype.updateBlockQuote = function (block) {
@ -272,12 +276,16 @@ const updateCtrl = ContentState => {
const { start, end } = selection.getCursorRange() const { start, end } = selection.getCursorRange()
const key = start.key const key = start.key
const block = this.getBlock(key) const block = this.getBlock(key)
// bugfix: #67 problem 1 // bugfix: #67 problem 1
if (block && block.icon) return event.preventDefault() if (block && block.icon) return event.preventDefault()
if (isMetaKey(event)) {
return
}
const { start: oldStart, end: oldEnd } = this.cursor const { start: oldStart, end: oldEnd } = this.cursor
if (event.type === 'keyup' && (event.key === 'ArrowUp' || event.key === 'ArrowDown') && floatBox.show) { if (event.type === 'keyup' && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) && floatBox.show) {
return return
} }
@ -322,7 +330,6 @@ const updateCtrl = ContentState => {
end.offset !== oldEnd.offset end.offset !== oldEnd.offset
) { ) {
this.cursor = { start, end } this.cursor = { start, end }
// this.render()
} }
return return
} }
@ -331,6 +338,7 @@ const updateCtrl = ContentState => {
const paragraph = document.querySelector(`#${key}`) const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ]) let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
let needRender = false let needRender = false
let isPartialRender = false
if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') { if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
// first cell in thead // first cell in thead
const cursorBlock = block.children[1].children[0].children[0].children[0] const cursorBlock = block.children[1].children[0].children[0].children[0]
@ -342,6 +350,7 @@ const updateCtrl = ContentState => {
} }
return this.render() return this.render()
} }
// remove temp block which generated by operation on code block // remove temp block which generated by operation on code block
if (block && block.key !== oldKey) { if (block && block.key !== oldKey) {
let oldBlock = this.getBlock(oldKey) let oldBlock = this.getBlock(oldKey)
@ -362,7 +371,7 @@ const updateCtrl = ContentState => {
if (block && block.type === 'pre') { if (block && block.type === 'pre') {
if (block.key !== oldKey) { if (block.key !== oldKey) {
this.cursor = lastCursor = { start, end } this.cursor = lastCursor = { start, end }
this.render() if (event.type === 'click') this.render()
} }
return return
} }
@ -405,12 +414,16 @@ const updateCtrl = ContentState => {
needRender = true needRender = true
} }
if (key === oldKey) {
isPartialRender = true
}
this.cursor = lastCursor = { start, end } this.cursor = lastCursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender(block) const checkMarkedUpdate = this.checkNeedRender(block)
const checkInlineUpdate = this.isCollapse() && this.checkInlineUpdate(block) const checkInlineUpdate = this.isCollapse() && this.checkInlineUpdate(block)
if (checkMarkedUpdate || checkInlineUpdate || needRender) { if (checkMarkedUpdate || checkInlineUpdate || needRender) {
this.render() isPartialRender && !checkInlineUpdate ? this.partialRender(block) : this.render()
} }
} }
} }

View File

@ -1,7 +1,7 @@
.ag-float-box { .ag-float-box {
position: absolute; position: absolute;
left: -10000px; left: -1000px;
top: -10000px; top: -1000px;
opacity: 0; opacity: 0;
max-height: 168px; max-height: 168px;
min-width: 130px; min-width: 130px;
@ -12,7 +12,7 @@
border-radius: 4px; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
list-style: none; list-style: none;
transition: opacity .4s ease-in; transition: opacity .15s ease-in;
overflow: auto; overflow: auto;
background: #fff; background: #fff;
z-index: 10000; z-index: 10000;

View File

@ -110,8 +110,8 @@ class Aganippe {
dispatchChange () { dispatchChange () {
const { eventCenter } = this const { eventCenter } = this
const markdown = this.getMarkdown() const markdown = this.markdown = this.getMarkdown()
const wordCount = this.getWordCount() const wordCount = this.getWordCount(markdown)
const cursor = this.getCursor() const cursor = this.getCursor()
eventCenter.dispatch('change', markdown, wordCount, cursor) eventCenter.dispatch('change', markdown, wordCount, cursor)
} }
@ -435,13 +435,12 @@ class Aganippe {
} }
exportUnstylishHtml () { exportUnstylishHtml () {
const blocks = this.contentState.getBlocks() const { markdown } = this
const markdown = new ExportMarkdown(blocks).generate()
return exportHtml(markdown) return exportHtml(markdown)
} }
getWordCount () { getWordCount (markdown) {
return this.contentState.wordCount() return this.contentState.wordCount(markdown)
} }
getCursor () { getCursor () {

View File

@ -1,5 +1,5 @@
import virtualize from 'snabbdom-virtualize/strings' import virtualize from 'snabbdom-virtualize/strings'
import { LOWERCASE_TAGS, CLASS_OR_ID, IMAGE_EXT_REG } from '../config' import { CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils' import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils'
import { insertAfter, operateClassName } from '../utils/domManipulate' import { insertAfter, operateClassName } from '../utils/domManipulate'
import { tokenizer } from './parse' import { tokenizer } from './parse'
@ -19,6 +19,7 @@ class StateRender {
constructor (eventCenter) { constructor (eventCenter) {
this.eventCenter = eventCenter this.eventCenter = eventCenter
this.loadImageMap = new Map() this.loadImageMap = new Map()
this.tokenCache = new Map()
this.container = null this.container = null
} }
@ -52,11 +53,16 @@ class StateRender {
} }
/** /**
* [render]: 2 steps: * [render All blocks]
* render vdom * @param {[Array]} blocks [description]
* @param {[Object]} cursor [description]
* @param {[Object]} activeBlocks [description]
* @param {[Array]} matches [description]
* @return {[undefined]} [description]
*/ */
render (blocks, cursor, activeBlocks, matches) { render (blocks, cursor, activeBlocks, matches) {
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}` const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
const emptyPres = []
const renderBlock = block => { const renderBlock = block => {
const type = block.type === 'hr' ? 'p' : block.type const type = block.type === 'hr' ? 'p' : block.type
@ -145,7 +151,14 @@ class StateRender {
const { text } = block const { text } = block
let children = '' let children = ''
if (text) { if (text) {
children = tokenizer(text, highlights).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], []) let tokens = null
if (highlights.length === 0 && this.tokenCache.has(text)) {
tokens = this.tokenCache.get(text)
} else {
tokens = tokenizer(text, highlights)
this.tokenCache.set(text, tokens)
}
children = tokens.reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
} }
if (/th|td/.test(block.type)) { if (/th|td/.test(block.type)) {
@ -179,12 +192,6 @@ class StateRender {
Object.assign(data.dataset, { role: block.type }) Object.assign(data.dataset, { role: block.type })
} }
if (block.type === 'pre') {
if (block.lang) Object.assign(data.dataset, { lang: block.lang })
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
children = ''
}
if (block.type === 'input') { if (block.type === 'input') {
const { checked, type, key } = block const { checked, type, key } = block
Object.assign(data.attrs, { type: 'checkbox' }) Object.assign(data.attrs, { type: 'checkbox' })
@ -200,6 +207,19 @@ class StateRender {
blockSelector += `.${CLASS_OR_ID['AG_TEMP']}` blockSelector += `.${CLASS_OR_ID['AG_TEMP']}`
} }
if (block.type === 'pre') {
const { key, lang } = block
const pre = document.querySelector(`pre#${key}`)
if (pre) {
operateClassName(pre, isActive ? 'add' : 'remove', CLASS_OR_ID['AG_ACTIVE'])
return toVNode(pre)
}
if (lang) Object.assign(data.dataset, { lang })
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
children = ''
emptyPres.push(key)
}
return h(blockSelector, data, children) return h(blockSelector, data, children)
} }
} }
@ -213,6 +233,48 @@ class StateRender {
const oldVdom = toVNode(rootDom) const oldVdom = toVNode(rootDom)
patch(oldVdom, newVdom) patch(oldVdom, newVdom)
return emptyPres
}
partialRender (block, cursor) {
const { key, text } = block
const type = block.type === 'hr' ? 'p' : block.type
let selector = `${type}#${key}`
const oldDom = document.querySelector(selector)
const oldVdom = toVNode(oldDom)
selector += `.${CLASS_OR_ID['AG_PARAGRAPH']}.${CLASS_OR_ID['AG_ACTIVE']}`
if (type === 'span') {
selector += `.${CLASS_OR_ID['AG_LINE']}`
}
const data = {
attrs: {},
dataset: {}
}
let children = ''
if (text) {
children = tokenizer(text, []).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
}
if (/th|td/.test(type)) {
const { align } = block
if (align) {
Object.assign(data.attrs, { style: `text-align:${align}` })
}
}
if (/^h\d$/.test(block.type)) {
Object.assign(data.dataset, { head: block.type })
}
if (/^h/.test(block.type)) { // h\d or hr
Object.assign(data.dataset, { role: block.type })
}
const newVdom = h(selector, data, children)
patch(oldVdom, newVdom)
} }
hr (h, cursor, block, token, outerClass) { hr (h, cursor, block, token, outerClass) {

View File

@ -335,22 +335,27 @@ const importRegister = ContentState => {
ContentState.prototype.importCursor = function (cursor) { ContentState.prototype.importCursor = function (cursor) {
// set cursor // set cursor
if (cursor) { const travel = blocks => {
const blocks = this.getArrayBlocks()
for (const block of blocks) { for (const block of blocks) {
const { text, key } = block const { key, text, children } = block
if (text) { if (text) {
const offset = block.text.indexOf(CURSOR_DNA) const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) { if (offset > -1) {
// remove the CURSOR_DNA in the block text
block.text = text.substring(0, offset) + text.substring(offset + CURSOR_DNA.length) block.text = text.substring(0, offset) + text.substring(offset + CURSOR_DNA.length)
this.cursor = { this.cursor = {
start: { key, offset }, start: { key, offset },
end: { key, offset } end: { key, offset }
} }
return
}
}
if (children.length) {
travel(children)
} }
} }
} }
if (cursor) {
travel(this.blocks)
} else { } else {
const lastBlock = this.getLastBlock() const lastBlock = this.getLastBlock()
const key = lastBlock.key const key = lastBlock.key

View File

@ -43,7 +43,7 @@
created () { created () {
this.$nextTick(() => { this.$nextTick(() => {
const { markdown = '', theme } = this const { markdown = '', theme, cursor } = this
const container = this.$refs.sourceCode const container = this.$refs.sourceCode
const codeMirrorConfig = { const codeMirrorConfig = {
value: markdown, value: markdown,
@ -60,11 +60,16 @@
} }
} }
if (theme === 'dark') codeMirrorConfig.theme = 'railscasts' if (theme === 'dark') codeMirrorConfig.theme = 'railscasts'
this.editor = codeMirror(container, codeMirrorConfig) const editor = this.editor = codeMirror(container, codeMirrorConfig)
bus.$on('file-loaded', this.setMarkdown) bus.$on('file-loaded', this.setMarkdown)
bus.$on('dotu-select', this.handleSelectDoutu) bus.$on('dotu-select', this.handleSelectDoutu)
this.listenChange() this.listenChange()
if (cursor) {
editor.setCursor(cursor)
} else {
setCursorAtLastLine(editor)
}
}) })
}, },
beforeDestroy () { beforeDestroy () {