✨ Add selection box extension
This commit is contained in:
@@ -421,6 +421,11 @@ export enum ExtensionID {
|
|||||||
ExtensionFold = "fold",
|
ExtensionFold = "fold",
|
||||||
ExtensionTextHighlight = "textHighlight",
|
ExtensionTextHighlight = "textHighlight",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择框
|
||||||
|
*/
|
||||||
|
ExtensionCheckbox = "checkbox",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI增强扩展
|
* UI增强扩展
|
||||||
* 小地图
|
* 小地图
|
||||||
|
@@ -193,6 +193,10 @@ export default {
|
|||||||
description: 'Highlight selected text content (Ctrl+Shift+H to toggle highlight)',
|
description: 'Highlight selected text content (Ctrl+Shift+H to toggle highlight)',
|
||||||
backgroundColor: 'Background Color',
|
backgroundColor: 'Background Color',
|
||||||
opacity: 'Opacity'
|
opacity: 'Opacity'
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
description: 'Render [x] and [ ] as interactive checkboxes'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -193,6 +193,10 @@ export default {
|
|||||||
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
|
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
|
||||||
backgroundColor: '背景颜色',
|
backgroundColor: '背景颜色',
|
||||||
opacity: '透明度'
|
opacity: '透明度'
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
name: '选择框',
|
||||||
|
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -19,6 +19,7 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito
|
|||||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||||
|
import {triggerFontChange} from '@/views/editor/extensions/checkbox';
|
||||||
|
|
||||||
export interface DocumentStats {
|
export interface DocumentStats {
|
||||||
lines: number;
|
lines: number;
|
||||||
@@ -259,6 +260,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
], () => {
|
], () => {
|
||||||
reconfigureFontSettings();
|
reconfigureFontSettings();
|
||||||
applyFontSize();
|
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 {hyperLink} from '../extensions/hyperlink'
|
||||||
import {minimap} from '../extensions/minimap'
|
import {minimap} from '../extensions/minimap'
|
||||||
import {vscodeSearch} from '../extensions/vscodeSearch'
|
import {vscodeSearch} from '../extensions/vscodeSearch'
|
||||||
|
import {createCheckboxExtension} from '../extensions/checkbox'
|
||||||
|
|
||||||
import {foldingOnIndent} from '../extensions/fold/foldExtension'
|
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类型错误
|
* 排除$zero值以避免TypeScript类型错误
|
||||||
@@ -186,6 +202,11 @@ const EXTENSION_CONFIGS = {
|
|||||||
factory: textHighlightFactory,
|
factory: textHighlightFactory,
|
||||||
displayNameKey: 'extensions.textHighlight.name',
|
displayNameKey: 'extensions.textHighlight.name',
|
||||||
descriptionKey: 'extensions.textHighlight.description'
|
descriptionKey: 'extensions.textHighlight.description'
|
||||||
|
},
|
||||||
|
[ExtensionID.ExtensionCheckbox]: {
|
||||||
|
factory: checkboxFactory,
|
||||||
|
displayNameKey: 'extensions.checkbox.name',
|
||||||
|
descriptionKey: 'extensions.checkbox.description'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ interface ConfigItemMeta {
|
|||||||
options?: SelectOption[]
|
options?: SelectOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扩展配置项元数据
|
// 只保留 select 类型的配置项元数据
|
||||||
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
|
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
|
||||||
[ExtensionID.ExtensionMinimap]: {
|
[ExtensionID.ExtensionMinimap]: {
|
||||||
displayText: {
|
displayText: {
|
||||||
@@ -124,13 +124,7 @@ const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItem
|
|||||||
{value: 'always', label: 'Always'},
|
{value: 'always', label: 'Always'},
|
||||||
{value: 'mouse-over', label: 'Mouse Over'}
|
{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'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取选择项选项
|
// 获取选择框的选项列表
|
||||||
const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => {
|
||||||
const meta = extensionConfigMeta[extensionId]?.[configKey]
|
return extensionConfigMeta[extensionId]?.[configKey]?.options || []
|
||||||
if (meta?.type === 'select' && meta.options) {
|
|
||||||
return meta.options
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -332,7 +322,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
|||||||
margin: 8px 0 16px 0;
|
margin: 8px 0 16px 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 13px; /* 调整字体大小 */
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-header {
|
.config-header {
|
||||||
@@ -343,7 +333,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-title {
|
.config-title {
|
||||||
font-size: 13px; /* 调整字体大小 */
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -351,7 +341,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
|||||||
|
|
||||||
.reset-button {
|
.reset-button {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 11px; /* 调整字体大小 */
|
font-size: 11px;
|
||||||
border: 1px solid var(--settings-input-border);
|
border: 1px solid var(--settings-input-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--settings-input-bg);
|
background-color: var(--settings-input-bg);
|
||||||
@@ -389,7 +379,7 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: var(--settings-input-bg);
|
background-color: var(--settings-input-bg);
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
font-size: 11px; /* 调整字体大小 */
|
font-size: 11px;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@@ -20,6 +20,7 @@ const (
|
|||||||
ExtensionColorSelector ExtensionID = "colorSelector" // 颜色选择器
|
ExtensionColorSelector ExtensionID = "colorSelector" // 颜色选择器
|
||||||
ExtensionFold ExtensionID = "fold"
|
ExtensionFold ExtensionID = "fold"
|
||||||
ExtensionTextHighlight ExtensionID = "textHighlight"
|
ExtensionTextHighlight ExtensionID = "textHighlight"
|
||||||
|
ExtensionCheckbox ExtensionID = "checkbox" // 选择框
|
||||||
|
|
||||||
// UI增强扩展
|
// UI增强扩展
|
||||||
ExtensionMinimap ExtensionID = "minimap" // 小地图
|
ExtensionMinimap ExtensionID = "minimap" // 小地图
|
||||||
@@ -94,6 +95,12 @@ func NewDefaultExtensions() []Extension {
|
|||||||
"opacity": 0.3,
|
"opacity": 0.3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: ExtensionCheckbox,
|
||||||
|
Enabled: true,
|
||||||
|
IsDefault: true,
|
||||||
|
Config: ExtensionConfig{},
|
||||||
|
},
|
||||||
|
|
||||||
// UI增强扩展
|
// UI增强扩展
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user