Add selection box extension

This commit is contained in:
2025-06-25 23:50:57 +08:00
parent a9b967aba4
commit 6f8775472d
8 changed files with 241 additions and 19 deletions

View File

@@ -421,6 +421,11 @@ export enum ExtensionID {
ExtensionFold = "fold",
ExtensionTextHighlight = "textHighlight",
/**
* 选择框
*/
ExtensionCheckbox = "checkbox",
/**
* UI增强扩展
* 小地图

View File

@@ -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'
}
}
};

View File

@@ -193,6 +193,10 @@ export default {
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
backgroundColor: '背景颜色',
opacity: '透明度'
},
checkbox: {
name: '选择框',
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
}
}
};

View File

@@ -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);
}
});
// 监听主题变化

View File

@@ -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<void>()
/**
* 复选框小部件类
*/
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
}

View File

@@ -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'
}
}

View File

@@ -108,7 +108,7 @@ interface ConfigItemMeta {
options?: SelectOption[]
}
// 扩展配置项元数据
// 只保留 select 类型的配置项元数据
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
[ExtensionID.ExtensionMinimap]: {
displayText: {
@@ -124,13 +124,7 @@ const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItem
{value: 'always', label: 'Always'},
{value: 'mouse-over', label: 'Mouse Over'}
]
},
autohide: {type: 'toggle'}
},
[ExtensionID.ExtensionTextHighlight]: {
backgroundColor: {type: 'text'},
opacity: {type: 'number'}
}
}
}
@@ -147,13 +141,9 @@ const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultV
return 'text'
}
// 获取选择项选项
const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
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 || []
}
</script>
@@ -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;