diff --git a/packages/cherry-markdown/src/Cherry.js b/packages/cherry-markdown/src/Cherry.js index 869d7eca8..496158bc6 100644 --- a/packages/cherry-markdown/src/Cherry.js +++ b/packages/cherry-markdown/src/Cherry.js @@ -1025,12 +1025,17 @@ export default class Cherry extends CherryStatic { * @private * @param {Event} _evt - 编辑事件对象(未使用) * @param {import('@codemirror/view').EditorView | Object} editorView - 编辑器实例 + * @param {import('@codemirror/state').ChangeDesc} change - 变更描述 */ - editText(_evt, editorView) { + editText(_evt, editorView, change) { try { // 兼容 CM6Adapter,如果传入的是 adapter,则获取其内部的 view const view = editorView.view || editorView; + if (!this.previewer.isPreviewerHidden() && this.dealPeerInsertChange(view, change)) { + return; + } + // 如果已有定时器,先清除,避免多次触发 if (this.timer) { clearTimeout(this.timer); @@ -1061,6 +1066,39 @@ export default class Cherry extends CherryStatic { } } + /** + * 处理单次新增普通字符的情况 + * @param {*} editorView + * @param {*} change + * @returns {boolean} + */ + dealPeerInsertChange(editorView, change) { + // 还没写完,先返回false + return false; + if (change.origin === '+input') { + const { text, from } = change; + // 只判断单光标输入的情况 + if (text.length !== 1) return false; + const insertStr = text[0]; + // 只判断输入中文、英文、数字的情况 + if (!/^[\u4e00-\u9fa5a-zA-Z0-9]+$/.test(insertStr)) return false; + const line = editorView.state.doc.lineAt(from); + const lineNum = line.number; + const { node, lines, blockLines } = this.previewer.$getTargetNodeByLineNum(lineNum); + if (!node) return false; + const beginLine = editorView.state.doc.line(Math.min(editorView.state.doc.lines, lines + 1)); + const endLine = editorView.state.doc.line(Math.min(editorView.state.doc.lines, lines + blockLines + 1)); + const md = editorView.state.sliceDoc(beginLine.from, endLine.to); + const html = this.engine.makeHtml(md); + const newNode = this.previewer.$createNodeByHtml(html); + if (!newNode) return false; + newNode[0].classList.add('cherry-highlight-line'); + this.previewer.$updateOneNode(this.previewer.getDomContainer(), node, newNode[0]); + return true; + } + return false; + } + /** * @private * @param {any} cb diff --git a/packages/cherry-markdown/src/Editor.js b/packages/cherry-markdown/src/Editor.js index d5c758bf8..5c439b5bb 100644 --- a/packages/cherry-markdown/src/Editor.js +++ b/packages/cherry-markdown/src/Editor.js @@ -1925,8 +1925,8 @@ export default class Editor { this.$cherry.$event.emit('focus', { evt, cherry: this.$cherry }); }); - editor.on('change', () => { - this.options.onChange(null, editor); + editor.on('change', (event, change) => { + this.options.onChange(null, editor, change); this.dealSpecialWords(); if (this.options.autoSave2Textarea) { textArea.value = editor.view.state.doc.toString(); diff --git a/packages/cherry-markdown/src/Previewer.js b/packages/cherry-markdown/src/Previewer.js index 032ed13a7..5796d74b4 100644 --- a/packages/cherry-markdown/src/Previewer.js +++ b/packages/cherry-markdown/src/Previewer.js @@ -728,54 +728,51 @@ export default class Previewer { } break; case 'update': - try { - let hasUpdate = false; - // 处理表格包含图表的特殊场景 - if ( - newContent[change.newIndex].dom.className === 'cherry-table-wrapper' && - newContent[change.newIndex].dom.querySelector('.cherry-table-figure .cherry-echarts-wrapper') && - oldContent[change.oldIndex].dom.querySelector('.cherry-table-figure .cherry-echarts-wrapper') - ) { - const oldWrapper = oldContent[change.oldIndex].dom.querySelector( - '.cherry-table-figure .cherry-echarts-wrapper', - ); - const newWrapper = newContent[change.newIndex].dom.querySelector( - '.cherry-table-figure .cherry-echarts-wrapper', - ); - oldWrapper.id = newWrapper.id; - oldWrapper.dataset.tableData = newWrapper.dataset.tableData; - oldWrapper.dataset.chartType = newWrapper.dataset.chartType; - oldWrapper.dataset.chartOptions = newWrapper.dataset.chartOptions; - oldContent[change.oldIndex].dom.dataset.sign = newContent[change.newIndex].dom.dataset.sign; - oldContent[change.oldIndex].dom.dataset.lines = newContent[change.newIndex].dom.dataset.lines; - this.$updateDom( - newContent[change.newIndex].dom.querySelector('.cherry-table'), - oldContent[change.oldIndex].dom.querySelector('.cherry-table'), - ); - hasUpdate = true; - } else if ( - // 处理代码块渲染echarts的特殊场景 - newContent[change.newIndex].dom.dataset.type === 'echarts' && - newContent[change.newIndex].dom.querySelector('.cherry-echarts-codeblock-wrapper') && - oldContent[change.oldIndex].dom.querySelector('.cherry-echarts-codeblock-wrapper') - ) { - oldContent[change.oldIndex].dom.dataset.sign = newContent[change.newIndex].dom.dataset.sign; - oldContent[change.oldIndex].dom.dataset.lines = newContent[change.newIndex].dom.dataset.lines; - hasUpdate = true; - } else if (newContent[change.newIndex].dom.querySelector('svg')) { - throw new Error(); // SVG暂不使用patch更新 - } - if (!hasUpdate) { - this.$updateDom(newContent[change.newIndex].dom, oldContent[change.oldIndex].dom); - } - } catch (e) { - domContainer.insertBefore(newContent[change.newIndex].dom, oldContent[change.oldIndex].dom); - domContainer.removeChild(oldContent[change.oldIndex].dom); - } + this.$updateOneNode(domContainer, oldContent[change.oldIndex].dom, newContent[change.newIndex].dom); } }); } + $updateOneNode(domContainer, oldNode, newNode) { + try { + let hasUpdate = false; + // 处理表格包含图表的特殊场景 + if ( + newNode.className === 'cherry-table-wrapper' && + newNode.querySelector('.cherry-table-figure .cherry-echarts-wrapper') && + oldNode.querySelector('.cherry-table-figure .cherry-echarts-wrapper') + ) { + const oldWrapper = oldNode.querySelector('.cherry-table-figure .cherry-echarts-wrapper'); + const newWrapper = newNode.querySelector('.cherry-table-figure .cherry-echarts-wrapper'); + oldWrapper.id = newWrapper.id; + oldWrapper.dataset.tableData = newWrapper.dataset.tableData; + oldWrapper.dataset.chartType = newWrapper.dataset.chartType; + oldWrapper.dataset.chartOptions = newWrapper.dataset.chartOptions; + oldNode.dataset.sign = newNode.dataset.sign; + oldNode.dataset.lines = newNode.dataset.lines; + this.$updateDom(newNode.querySelector('.cherry-table'), oldNode.querySelector('.cherry-table')); + hasUpdate = true; + } else if ( + // 处理代码块渲染echarts的特殊场景 + newNode.dataset.type === 'echarts' && + newNode.querySelector('.cherry-echarts-codeblock-wrapper') && + oldNode.querySelector('.cherry-echarts-codeblock-wrapper') + ) { + oldNode.dataset.sign = newNode.dataset.sign; + oldNode.dataset.lines = newNode.dataset.lines; + hasUpdate = true; + } else if (newNode.querySelector('svg')) { + throw new Error(); // SVG暂不使用patch更新 + } + if (!hasUpdate) { + this.$updateDom(newNode, oldNode); + } + } catch (e) { + domContainer.insertBefore(newNode, oldNode); + domContainer.removeChild(oldNode); + } + } + $dealUpdate(domContainer, oldHtmlList, newHtmlList) { if (newHtmlList.list !== oldHtmlList.list) { if (newHtmlList.list.length && oldHtmlList.list.length) { @@ -807,6 +804,19 @@ export default class Previewer { domContainer.innerHTML = html; } + $createNodeByHtml(html) { + if (typeof window.DOMParser !== 'undefined') { + // 如果支持DOMParser,则使用DOMParser将html字符串转成对应的HtmlElement + // 使用DOMParser是为了防止newHtml里的图片等资源自动加载 + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + return doc.querySelector('body').children; + } + const tmpDiv = document.createElement('div'); + tmpDiv.innerHTML = html; + return tmpDiv.children; + } + update(html) { // 销毁后不执行更新 if (this.isDestroyed) { @@ -823,18 +833,8 @@ export default class Previewer { if (this.editor?.selectAll) { domContainer.innerHTML = ''; } - let tmpDiv = null; - if (typeof window.DOMParser !== 'undefined') { - // 如果支持DOMParser,则使用DOMParser将html字符串转成对应的HtmlElement - // 使用DOMParser是为了防止newHtml里的图片等资源自动加载 - const parser = new DOMParser(); - const doc = parser.parseFromString(newHtml, 'text/html'); - tmpDiv = doc.querySelector('body'); - } else { - tmpDiv = document.createElement('div'); - tmpDiv.innerHTML = newHtml; - } - const newHtmlList = this.$getSignData(tmpDiv.children); + const newNode = this.$createNodeByHtml(newHtml); + const newHtmlList = this.$getSignData(newNode); const oldHtmlList = this.$getSignData(domContainer.children); try { @@ -1010,32 +1010,43 @@ export default class Previewer { return domContainer.scrollHeight; } const $lineNum = typeof lineNum === 'number' ? lineNum : parseInt(lineNum, 10); - const doms = /** @type {NodeListOf}*/ (domContainer.querySelectorAll('[data-sign]')); + const { node, lines, blockLines } = this.$getTargetNodeByLineNum($lineNum); + if (node) { + const { height: blockHeight, offsetTop } = getBlockTopAndHeightWithMargin(node); + const containerY = domContainer.offsetTop; + const blockY = offsetTop - containerY; + let scrollTo = blockY + blockHeight * linePercent; + // 区块多于1行时,按比例计算行偏移 + if (blockLines > 1) { + const overScrolledLines = blockLines - Math.abs($lineNum - (lines + blockLines)) - 1; + const overScrolledHeight = (overScrolledLines / blockLines) * blockHeight; + const blockLineHeight = blockHeight / blockLines; + scrollTo = blockY + overScrolledHeight + blockLineHeight * linePercent; + } + return scrollTo; + } + return domContainer.scrollHeight; + } + + $getTargetNodeByLineNum(lineNum) { + const domContainer = this.getDomContainer(); + const $lineNum = typeof lineNum === 'number' ? lineNum : parseInt(lineNum, 10); + const doms = domContainer.childNodes; let lines = 0; - const containerY = domContainer.offsetTop; for (let index = 0; index < doms.length; index++) { - if (doms[index].parentNode !== domContainer) { + const node = doms[index]; + if (!(node instanceof HTMLElement) || !node.dataset?.sign) { continue; } - const blockLines = parseInt(doms[index].getAttribute('data-lines'), 10); + const blockLines = parseInt(node.dataset.lines, 10); if (lines + blockLines < $lineNum) { lines += blockLines; continue; } else { - const { height: blockHeight, offsetTop } = getBlockTopAndHeightWithMargin(doms[index]); - const blockY = offsetTop - containerY; - let scrollTo = blockY + blockHeight * linePercent; - // 区块多于1行时,按比例计算行偏移 - if (blockLines > 1) { - const overScrolledLines = blockLines - Math.abs($lineNum - (lines + blockLines)) - 1; - const overScrolledHeight = (overScrolledLines / blockLines) * blockHeight; - const blockLineHeight = blockHeight / blockLines; - scrollTo = blockY + overScrolledHeight + blockLineHeight * linePercent; - } - return scrollTo; + return { node, lines, blockLines }; } } - return domContainer.scrollHeight; + return null; } /** diff --git a/packages/cherry-markdown/types/editor.d.ts b/packages/cherry-markdown/types/editor.d.ts index d162de992..3964106f0 100644 --- a/packages/cherry-markdown/types/editor.d.ts +++ b/packages/cherry-markdown/types/editor.d.ts @@ -343,7 +343,7 @@ export type EditorConfiguration = { onFocus: EditorEventCallback<'onFocus'>; onBlur: EditorEventCallback<'onBlur'>; onPaste: EditorEventCallback<'onPaste'>; - onChange: (update: ViewUpdate | null, codemirror: CM6Adapter) => void; + onChange: (update: ViewUpdate | null, codemirror: CM6Adapter, change: any | null) => void; onScroll: (editorView: EditorView) => void; handlePaste?: EditorPasteEventHandler; /** 预览区域跟随编辑器光标自动滚动 */