93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import { WidgetType, EditorView, Decoration } from '@codemirror/view';
|
|
import type { DecorationSet } from '@codemirror/view';
|
|
import { StateField, type Transaction, type EditorState, Range } from '@codemirror/state';
|
|
import { syntaxTree } from '@codemirror/language';
|
|
|
|
class ImagePreviewWidget extends WidgetType {
|
|
constructor(readonly url: string) {
|
|
super();
|
|
}
|
|
|
|
eq(other: ImagePreviewWidget) {
|
|
return this.url === other.url;
|
|
}
|
|
|
|
toDOM(view: EditorView) {
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'cm-image-preview-wrapper';
|
|
wrap.style.userSelect = 'none';
|
|
wrap.contentEditable = 'false';
|
|
// 块级预览的间距必须计入自身高度,避免外边距折叠导致点击坐标偏移。
|
|
wrap.style.padding = '8px 0';
|
|
|
|
const img = document.createElement('img');
|
|
img.onload = img.onerror = () => view.requestMeasure();
|
|
img.src = this.url;
|
|
img.className = 'cm-image-preview';
|
|
img.style.maxWidth = '100%';
|
|
img.style.maxHeight = '400px';
|
|
img.style.objectFit = 'contain';
|
|
img.style.display = 'block';
|
|
img.style.margin = '0';
|
|
img.style.border = '1px solid #e5e7eb';
|
|
|
|
wrap.appendChild(img);
|
|
return wrap;
|
|
}
|
|
|
|
destroy(dom: HTMLElement) {
|
|
const img = dom.querySelector('img');
|
|
if (img) {
|
|
img.onload = img.onerror = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getImageDecorationsForState(state: EditorState) {
|
|
const widgets: Range<Decoration>[] = [];
|
|
|
|
syntaxTree(state).iterate({
|
|
enter: (node: {
|
|
name: string;
|
|
from: number;
|
|
to: number;
|
|
node: { getChild: (type: string) => { from: number; to: number } | null };
|
|
}) => {
|
|
if (node.name === 'Image') {
|
|
// 查找图片语法中的 URL 节点
|
|
const urlNode = node.node.getChild('URL');
|
|
if (urlNode) {
|
|
let url = state.doc.sliceString(urlNode.from, urlNode.to);
|
|
// 处理可能的带尖括号的 URL
|
|
if (url.startsWith('<') && url.endsWith('>')) {
|
|
url = url.slice(1, -1);
|
|
}
|
|
|
|
const deco = Decoration.widget({
|
|
widget: new ImagePreviewWidget(url),
|
|
block: true, // 作为块级元素显示在下方
|
|
side: 1 // 插入在节点之后
|
|
});
|
|
|
|
widgets.push(deco.range(node.to));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return Decoration.set(widgets.sort((a, b) => a.from - b.from));
|
|
}
|
|
|
|
export const imagePreviewPlugin = StateField.define<DecorationSet>({
|
|
create(state) {
|
|
return getImageDecorationsForState(state);
|
|
},
|
|
update(value, tr: Transaction) {
|
|
if (tr.docChanged) {
|
|
return getImageDecorationsForState(tr.state);
|
|
}
|
|
return value;
|
|
},
|
|
provide: (f) => EditorView.decorations.from(f)
|
|
});
|