✨ Add selection box extension
This commit is contained in:
@@ -421,6 +421,11 @@ export enum ExtensionID {
|
||||
ExtensionFold = "fold",
|
||||
ExtensionTextHighlight = "textHighlight",
|
||||
|
||||
/**
|
||||
* 选择框
|
||||
*/
|
||||
ExtensionCheckbox = "checkbox",
|
||||
|
||||
/**
|
||||
* UI增强扩展
|
||||
* 小地图
|
||||
|
@@ -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'
|
||||
}
|
||||
}
|
||||
};
|
@@ -193,6 +193,10 @@ export default {
|
||||
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
|
||||
backgroundColor: '背景颜色',
|
||||
opacity: '透明度'
|
||||
},
|
||||
checkbox: {
|
||||
name: '选择框',
|
||||
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
|
||||
}
|
||||
}
|
||||
};
|
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听主题变化
|
||||
|
186
frontend/src/views/editor/extensions/checkbox/index.ts
Normal file
186
frontend/src/views/editor/extensions/checkbox/index.ts
Normal 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
|
||||
}
|
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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增强扩展
|
||||
{
|
||||
|
Reference in New Issue
Block a user