feat(web): add code copy button, block-scoped select-all, table toolbar polish

- Copy button in the top-right of fenced code blocks, with execCommand
  fallback when the Clipboard API is unavailable over plain HTTP
- Mod-A inside a code block selects the block content first; pressing
  it again selects the whole document
- Paint code block backgrounds on a ::before layer below CodeMirror's
  selection layer so selections inside code blocks are visible
- Table toolbar sits in reserved space above the table instead of
  overlapping it, with grouped icon buttons
This commit is contained in:
eezd committed 2026-10-16 18:07:42 +08:00
1 parent 54fecf71c8
commit 63f508d3df
5 files changed
+218 -59

No files matched your search

+15 -4
View File
@@ -2,8 +2,8 @@ import { syntaxTree } from '@codemirror/language'
import { Facet, type EditorState, type Range } from '@codemirror/state'
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view'
import type { SyntaxNode } from '@lezer/common'
import { fenceLanguage } from './codeblocks'
import { BulletWidget, CheckboxWidget, CodeLangWidget, HorizontalRuleWidget, ImageWidget } from './widgets'
import { codeContentRange, fenceLanguage, fencedCodeAt } from './codeblocks'
import { BulletWidget, CheckboxWidget, CodeHeaderWidget, HorizontalRuleWidget, ImageWidget } from './widgets'
// 把笔记中书写的图片地址解析为浏览器可加载的 URL(相对路径需结合笔记所在目录)
export const resolveImageUrl = Facet.define<(src: string) => string, (src: string) => string>({
@@ -181,12 +181,16 @@ function buildDecorations(view: EditorView): DecorationSet {
classLines(node.from, node.to, (n, first, last) =>
n === first ? 'cm-rm-code cm-rm-code-begin' : n === last && closed ? 'cm-rm-code cm-rm-code-end' : 'cm-rm-code',
)
if (isActive(node.from, node.to)) return
const info = node.getChild('CodeInfo')
const openLine = doc.lineAt(node.from)
const lang = info ? fenceLanguage(state, info.from, info.to) : ''
if (isActive(node.from, node.to)) {
// 编辑中显示围栏源码,复制按钮浮在行尾
out.push(Decoration.widget({ widget: new CodeHeaderWidget(lang, false, codeAt), side: 1 }).range(openLine.to))
return
}
out.push(lineDeco('cm-rm-code-fence').range(openLine.from))
out.push(Decoration.replace({ widget: new CodeLangWidget(lang) }).range(marks[0].from, openLine.to))
out.push(Decoration.replace({ widget: new CodeHeaderWidget(lang, true, codeAt) }).range(marks[0].from, openLine.to))
if (closed) {
const closeMark = marks[marks.length - 1]
out.push(lineDeco('cm-rm-code-fence').range(doc.lineAt(closeMark.from).from))
@@ -197,6 +201,13 @@ function buildDecorations(view: EditorView): DecorationSet {
return Decoration.set(out, true)
}
function codeAt(view: EditorView, pos: number): string | null {
const node = fencedCodeAt(view.state, pos)
if (!node) return null
const { from, to } = codeContentRange(view.state, node)
return view.state.sliceDoc(from, to)
}
function trailingSpaceStart(state: EditorState, pos: number): number {
let start = pos
while (start > 0 && state.sliceDoc(start - 1, start) === ' ') start--