This commit is contained in:
1 parent
8f2265c5bd
commit
71c59837eb
2 files changed
+134
-2
No files matched your search
@@ -21,7 +21,7 @@
|
|||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { addToast } from '$lib/stores/toast.svelte';
|
import { addToast } from '$lib/stores/toast.svelte';
|
||||||
import { imagePreviewPlugin } from '$lib/editor/imagePreviewPlugin';
|
import { imagePreviewPlugin } from '$lib/editor/imagePreviewPlugin';
|
||||||
import { tableFormatterPlugin } from '$lib/editor/tableFormatterPlugin';
|
import { tableFormatterPlugin, tablePreviewField } from '$lib/editor/tableFormatterPlugin';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value = $bindable(''),
|
value = $bindable(''),
|
||||||
@@ -123,6 +123,7 @@
|
|||||||
}),
|
}),
|
||||||
imagePreviewPlugin,
|
imagePreviewPlugin,
|
||||||
tableFormatterPlugin,
|
tableFormatterPlugin,
|
||||||
|
tablePreviewField,
|
||||||
customTheme,
|
customTheme,
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ViewPlugin, ViewUpdate, EditorView } from '@codemirror/view';
|
import { ViewPlugin, ViewUpdate, EditorView, WidgetType, 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';
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
|
||||||
// Helper to count CJK chars as 2 for better alignment
|
// Helper to count CJK chars as 2 for better alignment
|
||||||
@@ -164,3 +166,132 @@ export const tableFormatterPlugin = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class TablePreviewWidget extends WidgetType {
|
||||||
|
constructor(readonly tableStr: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: TablePreviewWidget) {
|
||||||
|
return this.tableStr === other.tableStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const lines = this.tableStr.trim().split('\n');
|
||||||
|
const rows = lines.map((line) => {
|
||||||
|
const content = line.trim();
|
||||||
|
const cols = content.split('|');
|
||||||
|
if (cols.length > 0 && cols[0].trim() === '') cols.shift();
|
||||||
|
if (cols.length > 0 && cols[cols.length - 1].trim() === '') cols.pop();
|
||||||
|
return cols.map((c) => c.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'cm-table-preview-wrapper';
|
||||||
|
container.style.userSelect = 'none';
|
||||||
|
container.contentEditable = 'false';
|
||||||
|
container.style.overflowX = 'auto';
|
||||||
|
container.style.margin = '1.5rem 0';
|
||||||
|
container.style.cursor = 'pointer';
|
||||||
|
container.title = '点击编辑表格 (Click to edit table)';
|
||||||
|
|
||||||
|
if (rows.length === 0) return container;
|
||||||
|
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.style.width = '100%';
|
||||||
|
table.style.borderCollapse = 'collapse';
|
||||||
|
|
||||||
|
const alignments: string[] = [];
|
||||||
|
if (rows.length > 1) {
|
||||||
|
rows[1].forEach((col) => {
|
||||||
|
const left = col.startsWith(':');
|
||||||
|
const right = col.endsWith(':');
|
||||||
|
if (left && right) alignments.push('center');
|
||||||
|
else if (right) alignments.push('right');
|
||||||
|
else alignments.push('left');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
if (rowIndex === 1) return; // Skip separator
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const isHeader = rowIndex === 0;
|
||||||
|
|
||||||
|
row.forEach((col, colIndex) => {
|
||||||
|
const td = document.createElement(isHeader ? 'th' : 'td');
|
||||||
|
// Basic markdown parsing for links and strong could be done here, but textContent is safer
|
||||||
|
td.textContent = col;
|
||||||
|
td.style.border = '1px solid var(--border-color, #e5e7eb)';
|
||||||
|
td.style.padding = '0.6rem 1rem';
|
||||||
|
td.style.textAlign = alignments[colIndex] || 'left';
|
||||||
|
if (isHeader) {
|
||||||
|
td.style.backgroundColor = 'rgba(0, 0, 0, 0.03)';
|
||||||
|
td.style.fontWeight = '600';
|
||||||
|
}
|
||||||
|
tr.appendChild(td);
|
||||||
|
});
|
||||||
|
table.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(table);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableDecorations(state: EditorState) {
|
||||||
|
const widgets: Range<Decoration>[] = [];
|
||||||
|
const pos = state.selection.main.head;
|
||||||
|
let currentTableRange: { from: number; to: number } | null = null;
|
||||||
|
|
||||||
|
// First find if the cursor is currently inside ANY table
|
||||||
|
syntaxTree(state).iterate({
|
||||||
|
from: pos,
|
||||||
|
to: pos,
|
||||||
|
enter: (node: { name: string; from: number; to: number }) => {
|
||||||
|
if (node.name === 'Table') {
|
||||||
|
currentTableRange = { from: node.from, to: node.to };
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then find all tables and replace those that the cursor is NOT inside
|
||||||
|
syntaxTree(state).iterate({
|
||||||
|
enter: (node: { name: string; from: number; to: number }) => {
|
||||||
|
if (node.name === 'Table') {
|
||||||
|
const isCurrent =
|
||||||
|
currentTableRange &&
|
||||||
|
node.from === currentTableRange.from &&
|
||||||
|
node.to === currentTableRange.to;
|
||||||
|
|
||||||
|
if (!isCurrent) {
|
||||||
|
const tableStr = state.doc.sliceString(node.from, node.to);
|
||||||
|
// Skip malformed tables or empty ones
|
||||||
|
if (tableStr.trim().length > 0) {
|
||||||
|
const deco = Decoration.replace({
|
||||||
|
widget: new TablePreviewWidget(tableStr),
|
||||||
|
block: true
|
||||||
|
});
|
||||||
|
widgets.push(deco.range(node.from, node.to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Decoration.set(widgets.sort((a, b) => a.from - b.from));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tablePreviewField = StateField.define<DecorationSet>({
|
||||||
|
create(state) {
|
||||||
|
return getTableDecorations(state);
|
||||||
|
},
|
||||||
|
update(value, tr: Transaction) {
|
||||||
|
if (tr.docChanged || tr.selection) {
|
||||||
|
return getTableDecorations(tr.state);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f)
|
||||||
|
});
|
||||||
Reference in new issue
Block a user