Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/cherry-markdown/src/Cherry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/cherry-markdown/src/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
155 changes: 83 additions & 72 deletions packages/cherry-markdown/src/Previewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -1010,32 +1010,43 @@ export default class Previewer {
return domContainer.scrollHeight;
}
const $lineNum = typeof lineNum === 'number' ? lineNum : parseInt(lineNum, 10);
const doms = /** @type {NodeListOf<HTMLElement>}*/ (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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/cherry-markdown/types/editor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/** 预览区域跟随编辑器光标自动滚动 */
Expand Down
Loading