From 6f8775472d1716183b1d8abb5d3998efd0e86c32 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Wed, 25 Jun 2025 23:50:57 +0800 Subject: [PATCH] :sparkles: Add selection box extension --- .../voidraft/internal/models/models.ts | 5 + frontend/src/i18n/locales/en-US.ts | 4 + frontend/src/i18n/locales/zh-CN.ts | 4 + frontend/src/stores/editorStore.ts | 5 + .../views/editor/extensions/checkbox/index.ts | 186 ++++++++++++++++++ .../src/views/editor/manager/factories.ts | 21 ++ .../views/settings/pages/ExtensionsPage.vue | 28 +-- internal/models/extensions.go | 7 + 8 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 frontend/src/views/editor/extensions/checkbox/index.ts diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index de5ea94..270c4b7 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -421,6 +421,11 @@ export enum ExtensionID { ExtensionFold = "fold", ExtensionTextHighlight = "textHighlight", + /** + * 选择框 + */ + ExtensionCheckbox = "checkbox", + /** * UI增强扩展 * 小地图 diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index dce8e58..793e61a 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -193,6 +193,10 @@ export default { description: 'Highlight selected text content (Ctrl+Shift+H to toggle highlight)', backgroundColor: 'Background Color', opacity: 'Opacity' + }, + checkbox: { + name: 'Checkbox', + description: 'Render [x] and [ ] as interactive checkboxes' } } }; \ No newline at end of file diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 293adc2..76b651c 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -193,6 +193,10 @@ export default { description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)', backgroundColor: '背景颜色', opacity: '透明度' + }, + checkbox: { + name: '选择框', + description: '将 [x] 和 [ ] 渲染为可交互的选择框' } } }; \ No newline at end of file diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index b615f7c..a733172 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -19,6 +19,7 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager'; import {useExtensionStore} from './extensionStore'; import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; +import {triggerFontChange} from '@/views/editor/extensions/checkbox'; export interface DocumentStats { lines: number; @@ -259,6 +260,10 @@ export const useEditorStore = defineStore('editor', () => { ], () => { reconfigureFontSettings(); applyFontSize(); + // 通知checkbox扩展字体已变化,需要重新渲染 + if (editorView.value) { + triggerFontChange(editorView.value as EditorView); + } }); // 监听主题变化 diff --git a/frontend/src/views/editor/extensions/checkbox/index.ts b/frontend/src/views/editor/extensions/checkbox/index.ts new file mode 100644 index 0000000..5fd9d55 --- /dev/null +++ b/frontend/src/views/editor/extensions/checkbox/index.ts @@ -0,0 +1,186 @@ +import { EditorView, Decoration } from "@codemirror/view" +import { WidgetType } from "@codemirror/view" +import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view" +import { Extension, Compartment, StateEffect } from "@codemirror/state" + +// 创建字体变化效果 +const fontChangeEffect = StateEffect.define() + +/** + * 复选框小部件类 + */ +class CheckboxWidget extends WidgetType { + constructor(readonly checked: boolean) { + super() + } + + eq(other: CheckboxWidget) { + return other.checked == this.checked + } + + toDOM() { + let wrap = document.createElement("span") + wrap.setAttribute("aria-hidden", "true") + wrap.className = "cm-checkbox-toggle" + + let box = document.createElement("input") + box.type = "checkbox" + box.checked = this.checked + box.tabIndex = -1 + box.style.margin = "0" + box.style.padding = "0" + box.style.cursor = "pointer" + box.style.position = "relative" + box.style.top = "0.1em" + box.style.marginRight = "0.5em" + // 设置相对单位,让复选框跟随字体大小变化 + box.style.width = "1em" + box.style.height = "1em" + + wrap.appendChild(box) + return wrap + } + + ignoreEvent() { + return false + } +} + +/** + * 查找并创建复选框装饰 + */ +function findCheckboxes(view: EditorView) { + let widgets: any = [] + const doc = view.state.doc + + for (let { from, to } of view.visibleRanges) { + // 使用正则表达式查找 [x] 或 [ ] 模式 + const text = doc.sliceString(from, to) + const checkboxRegex = /\[([ x])\]/gi + let match + + while ((match = checkboxRegex.exec(text)) !== null) { + const matchPos = from + match.index + const matchEnd = matchPos + match[0].length + + // 检查前后是否有合适的空格或行首 + const beforeChar = matchPos > 0 ? doc.sliceString(matchPos - 1, matchPos) : "" + const afterChar = matchEnd < doc.length ? doc.sliceString(matchEnd, matchEnd + 1) : "" + + // 只在行首或空格后,且后面跟空格或行尾时才渲染 + if ((beforeChar === "" || beforeChar === " " || beforeChar === "\t" || beforeChar === "\n") && + (afterChar === "" || afterChar === " " || afterChar === "\t" || afterChar === "\n")) { + + const isChecked = match[1].toLowerCase() === "x" + let deco = Decoration.replace({ + widget: new CheckboxWidget(isChecked), + inclusive: false, + }) + widgets.push(deco.range(matchPos, matchEnd)) + } + } + } + + return Decoration.set(widgets) +} + +/** + * 切换复选框状态 + */ +function toggleCheckbox(view: EditorView, pos: number) { + const doc = view.state.doc + + // 查找当前位置附近的复选框模式 + for (let offset = -3; offset <= 0; offset++) { + const checkPos = pos + offset + if (checkPos >= 0 && checkPos + 3 <= doc.length) { + const text = doc.sliceString(checkPos, checkPos + 3).toLowerCase() + let change + + if (text === "[x]") { + change = { from: checkPos, to: checkPos + 3, insert: "[ ]" } + } else if (text === "[ ]") { + change = { from: checkPos, to: checkPos + 3, insert: "[x]" } + } + + if (change) { + view.dispatch({ changes: change }) + return true + } + } + } + return false +} + +// 创建字体变化效果的便捷函数 +export const triggerFontChange = (view: EditorView) => { + view.dispatch({ + effects: fontChangeEffect.of(undefined) + }) +} + +/** + * 创建复选框扩展 + */ +export function createCheckboxExtension(): Extension { + return [ + // 主要的复选框插件 + ViewPlugin.fromClass(class { + decorations: DecorationSet + + constructor(view: EditorView) { + this.decorations = findCheckboxes(view) + } + + update(update: ViewUpdate) { + // 检查是否需要重新渲染复选框 + const shouldUpdate = update.docChanged || + update.viewportChanged || + update.geometryChanged || + update.transactions.some(tr => tr.effects.some(e => e.is(fontChangeEffect))) + + if (shouldUpdate) { + this.decorations = findCheckboxes(update.view) + } + } + }, { + decorations: v => v.decorations, + + eventHandlers: { + mousedown: (e, view) => { + let target = e.target as HTMLElement + if (target.nodeName == "INPUT" && target.parentElement!.classList.contains("cm-checkbox-toggle")) { + const pos = view.posAtDOM(target) + return toggleCheckbox(view, pos) + } + } + } + }), + + // 复选框样式 + EditorView.theme({ + ".cm-checkbox-toggle": { + display: "inline-block", + verticalAlign: "baseline", + }, + ".cm-checkbox-toggle input[type=checkbox]": { + margin: "0", + padding: "0", + verticalAlign: "baseline", + cursor: "pointer", + // 确保复选框大小跟随字体 + fontSize: "inherit", + } + }) + ] +} + +// 默认导出 +export const checkboxExtension = createCheckboxExtension() + +// 导出类型和工具函数 +export { + CheckboxWidget, + toggleCheckbox, + findCheckboxes +} \ No newline at end of file diff --git a/frontend/src/views/editor/manager/factories.ts b/frontend/src/views/editor/manager/factories.ts index 47cbefb..3f7ff3b 100644 --- a/frontend/src/views/editor/manager/factories.ts +++ b/frontend/src/views/editor/manager/factories.ts @@ -10,6 +10,7 @@ import {color} from '../extensions/colorSelector' import {hyperLink} from '../extensions/hyperlink' import {minimap} from '../extensions/minimap' import {vscodeSearch} from '../extensions/vscodeSearch' +import {createCheckboxExtension} from '../extensions/checkbox' import {foldingOnIndent} from '../extensions/fold/foldExtension' @@ -139,6 +140,21 @@ export const foldFactory: ExtensionFactory = { } } +/** + * 选择框扩展工厂 + */ +export const checkboxFactory: ExtensionFactory = { + create(config: any) { + return createCheckboxExtension() + }, + getDefaultConfig() { + return {} + }, + validateConfig(config: any) { + return typeof config === 'object' + } +} + /** * 所有扩展的统一配置 * 排除$zero值以避免TypeScript类型错误 @@ -186,6 +202,11 @@ const EXTENSION_CONFIGS = { factory: textHighlightFactory, displayNameKey: 'extensions.textHighlight.name', descriptionKey: 'extensions.textHighlight.description' + }, + [ExtensionID.ExtensionCheckbox]: { + factory: checkboxFactory, + displayNameKey: 'extensions.checkbox.name', + descriptionKey: 'extensions.checkbox.description' } } diff --git a/frontend/src/views/settings/pages/ExtensionsPage.vue b/frontend/src/views/settings/pages/ExtensionsPage.vue index 248307c..df49f72 100644 --- a/frontend/src/views/settings/pages/ExtensionsPage.vue +++ b/frontend/src/views/settings/pages/ExtensionsPage.vue @@ -108,7 +108,7 @@ interface ConfigItemMeta { options?: SelectOption[] } -// 扩展配置项元数据 +// 只保留 select 类型的配置项元数据 const extensionConfigMeta: Partial>> = { [ExtensionID.ExtensionMinimap]: { displayText: { @@ -124,13 +124,7 @@ const extensionConfigMeta: Partial { - const meta = extensionConfigMeta[extensionId]?.[configKey] - if (meta?.type === 'select' && meta.options) { - return meta.options - } - return [] +// 获取选择框的选项列表 +const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => { + return extensionConfigMeta[extensionId]?.[configKey]?.options || [] } @@ -332,7 +322,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => { margin: 8px 0 16px 0; padding: 12px; border-radius: 6px; - font-size: 13px; /* 调整字体大小 */ + font-size: 13px; } .config-header { @@ -343,7 +333,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => { } .config-title { - font-size: 13px; /* 调整字体大小 */ + font-size: 13px; font-weight: 600; color: var(--settings-text); margin: 0; @@ -351,7 +341,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => { .reset-button { padding: 6px 12px; - font-size: 11px; /* 调整字体大小 */ + font-size: 11px; border: 1px solid var(--settings-input-border); border-radius: 4px; background-color: var(--settings-input-bg); @@ -389,7 +379,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => { border-radius: 3px; background-color: var(--settings-input-bg); color: var(--settings-text); - font-size: 11px; /* 调整字体大小 */ + font-size: 11px; &:focus { outline: none; diff --git a/internal/models/extensions.go b/internal/models/extensions.go index 547d027..f2d29ce 100644 --- a/internal/models/extensions.go +++ b/internal/models/extensions.go @@ -20,6 +20,7 @@ const ( ExtensionColorSelector ExtensionID = "colorSelector" // 颜色选择器 ExtensionFold ExtensionID = "fold" ExtensionTextHighlight ExtensionID = "textHighlight" + ExtensionCheckbox ExtensionID = "checkbox" // 选择框 // UI增强扩展 ExtensionMinimap ExtensionID = "minimap" // 小地图 @@ -94,6 +95,12 @@ func NewDefaultExtensions() []Extension { "opacity": 0.3, }, }, + { + ID: ExtensionCheckbox, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, // UI增强扩展 {