feat: 添加 Markdown 编辑器图片预览插件
This commit is contained in:
1 parent
acb05de1c0
commit
1282172c51
1 file changed
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import { WidgetType, EditorView, ViewPlugin, Decoration, ViewUpdate } from '@codemirror/view';
|
||||||
|
import type { DecorationSet } from '@codemirror/view';
|
||||||
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
import { Range } from '@codemirror/state';
|
||||||
|
|
||||||
|
class ImagePreviewWidget extends WidgetType {
|
||||||
|
constructor(readonly url: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: ImagePreviewWidget) {
|
||||||
|
return this.url === other.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'cm-image-preview-wrapper';
|
||||||
|
wrap.style.userSelect = 'none';
|
||||||
|
wrap.contentEditable = 'false';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
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.marginTop = '8px';
|
||||||
|
img.style.marginBottom = '8px';
|
||||||
|
img.style.borderRadius = '4px';
|
||||||
|
img.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||||
|
|
||||||
|
wrap.appendChild(img);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageDecorations(view: EditorView) {
|
||||||
|
const widgets: Range<Decoration>[] = [];
|
||||||
|
const { state } = view;
|
||||||
|
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
syntaxTree(state).iterate({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
enter: (node: any) => {
|
||||||
|
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 = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations: DecorationSet;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = getImageDecorations(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
// 文档改变或视口改变时重新计算
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = getImageDecorations(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations
|
||||||
|
}
|
||||||
|
);
|
||||||
Reference in new issue
Block a user