🎨 Refactor and optimize code

This commit is contained in:
2025-10-05 00:58:27 +08:00
parent c22e349181
commit d49ffc20df
16 changed files with 655 additions and 674 deletions

View File

@@ -16,22 +16,22 @@
</div> </div>
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')"> <div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M3 12h18M3 18h18"/> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<path d="M18 6L6 18M6 6l12 12"/> <path d="M9 9l6 6M15 9l-6 6"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
</div> </div>
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')"> <div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/> <path d="M15 18l-6-6 6-6"/>
<path d="M18 6L6 18M6 6l12 12"/> <path d="M9 18l-6-6 6-6"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
</div> </div>
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')"> <div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6"/> <path d="M9 18l6-6-6-6"/>
<path d="M18 6L6 18M6 6l12 12"/> <path d="M15 18l6-6-6-6"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeRight') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeRight') }}</span>
</div> </div>
@@ -136,7 +136,7 @@ onUnmounted(() => {
border-radius: 4px; border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
overflow: hidden; overflow: hidden;
min-width: 140px; min-width: 100px;
user-select: none; user-select: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
} }
@@ -147,7 +147,7 @@ onUnmounted(() => {
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: var(--text-primary); color: var(--text-muted);
transition: all 0.15s ease; transition: all 0.15s ease;
gap: 8px; gap: 8px;
@@ -165,7 +165,7 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
width: 12px; width: 12px;
height: 12px; height: 12px;
color: var(--text-secondary); color: var(--text-muted);
transition: color 0.15s ease; transition: color 0.15s ease;
.menu-item:hover & { .menu-item:hover & {
@@ -178,51 +178,4 @@ onUnmounted(() => {
font-weight: 400; font-weight: 400;
flex: 1; flex: 1;
} }
/* 主题适配 */
:root[data-theme="dark"] .tab-context-menu {
background-color: var(--bg-secondary);
border-color: var(--border-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.menu-item {
color: var(--text-primary);
&:hover {
background-color: var(--toolbar-button-hover);
color: #ffffff;
}
}
.menu-icon {
color: var(--text-secondary);
.menu-item:hover & {
color: #ffffff;
}
}
}
:root[data-theme="light"] .tab-context-menu {
background-color: var(--bg-secondary);
border-color: var(--border-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.menu-item {
color: var(--text-primary);
&:hover {
background-color: var(--toolbar-button-hover);
color: #000000;
}
}
.menu-icon {
color: var(--text-secondary);
.menu-item:hover & {
color: #000000;
}
}
}
</style> </style>

View File

@@ -236,7 +236,7 @@ const handleContextMenu = (event: MouseEvent) => {
&:hover { &:hover {
background-color: var(--error-color); background-color: var(--error-color);
color: white; color: var(--text-muted);
opacity: 1 !important; opacity: 1 !important;
} }

View File

@@ -35,6 +35,12 @@ export const useDocumentStore = defineStore('document', () => {
// === 错误处理 === // === 错误处理 ===
const setError = (docId: number, message: string) => { const setError = (docId: number, message: string) => {
selectorError.value = {docId, message}; selectorError.value = {docId, message};
// 3秒后自动清除错误状态
setTimeout(() => {
if (selectorError.value?.docId === docId) {
selectorError.value = null;
}
}, 3000);
}; };
const clearError = () => { const clearError = () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import {onBeforeUnmount, onMounted, ref} from 'vue'; import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
@@ -17,6 +17,8 @@ const tabStore = useTabStore();
const editorElement = ref<HTMLElement | null>(null); const editorElement = ref<HTMLElement | null>(null);
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
// 创建滚轮缩放处理器 // 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler( const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize, configStore.increaseFontSize,
@@ -53,7 +55,7 @@ onBeforeUnmount(() => {
<template> <template>
<div class="editor-container"> <div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading && configStore.config.general?.enableLoadingAnimation" text="VOIDRAFT"/> <LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
<div ref="editorElement" class="editor"></div> <div ref="editorElement" class="editor"></div>
<Toolbar/> <Toolbar/>
</div> </div>

View File

@@ -1,7 +1,5 @@
/** /**
* 上下文菜单视图实现 * 上下文菜单视图实现
* 处理菜单的创建、定位和事件绑定
* 优化为单例模式避免频繁创建和销毁DOM元素
*/ */
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
@@ -15,97 +13,253 @@ declare global {
} }
} }
// 菜单DOM元素缓存
let menuElement: HTMLElement | null = null;
let clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
// 子菜单缓存池
const submenuPool: Map<string, HTMLElement> = new Map();
/** /**
* 获取或创建菜单DOM元素 * 菜单项元素池,用于复用DOM元素
*/ */
function getOrCreateMenuElement(): HTMLElement { class MenuItemPool {
if (!menuElement) { private pool: HTMLElement[] = [];
menuElement = document.createElement("div"); private maxPoolSize = 50; // 最大池大小
menuElement.className = "cm-context-menu";
menuElement.style.display = "none";
document.body.appendChild(menuElement);
// 阻止菜单内右键点击冒泡 /**
menuElement.addEventListener('contextmenu', (e) => { * 获取或创建菜单项元素
e.preventDefault(); */
e.stopPropagation(); get(): HTMLElement {
return false; if (this.pool.length > 0) {
}); return this.pool.pop()!;
}
const menuItem = document.createElement("div");
menuItem.className = "cm-context-menu-item";
return menuItem;
}
/**
* 回收菜单项元素
*/
release(element: HTMLElement): void {
if (this.pool.length < this.maxPoolSize) {
// 清理元素状态
element.className = "cm-context-menu-item";
element.innerHTML = "";
element.style.cssText = "";
// 移除所有事件监听器(通过克隆节点)
const cleanElement = element.cloneNode(false) as HTMLElement;
this.pool.push(cleanElement);
}
}
/**
* 清空池
*/
clear(): void {
this.pool.length = 0;
} }
return menuElement;
} }
/** /**
* 创建或获取子菜单元素 * 上下文菜单管理器
* @param id 子菜单唯一标识
*/ */
function getOrCreateSubmenu(id: string): HTMLElement { class ContextMenuManager {
if (!submenuPool.has(id)) { private static instance: ContextMenuManager;
const submenu = document.createElement("div");
submenu.className = "cm-context-menu cm-context-submenu";
submenu.style.display = "none";
document.body.appendChild(submenu);
submenuPool.set(id, submenu);
// 阻止子菜单点击事件冒泡 private menuElement: HTMLElement | null = null;
submenu.addEventListener('click', (e) => { private submenuPool: Map<string, HTMLElement> = new Map();
e.stopPropagation(); private menuItemPool = new MenuItemPool();
}); private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
} private keyDownHandler: ((e: KeyboardEvent) => void) | null = null;
return submenuPool.get(id)!; private currentView: EditorView | null = null;
} private activeSubmenus: Set<HTMLElement> = new Set();
private ripplePool: HTMLElement[] = [];
/** // 事件委托处理器
* 创建菜单项DOM元素 private menuClickHandler: ((e: MouseEvent) => void) | null = null;
*/ private menuMouseHandler: ((e: MouseEvent) => void) | null = null;
function createMenuItemElement(item: MenuItem, view: EditorView): HTMLElement {
// 创建菜单项容器
const menuItem = document.createElement("div");
menuItem.className = "cm-context-menu-item";
// 如果有子菜单,添加相应类 private constructor() {
if (item.submenu && item.submenu.length > 0) { this.initializeEventHandlers();
menuItem.classList.add("cm-context-menu-item-with-submenu");
} }
// 创建内容容器 /**
const contentContainer = document.createElement("div"); * 获取单例实例
contentContainer.className = "cm-context-menu-item-label"; */
static getInstance(): ContextMenuManager {
// 标签文本 if (!ContextMenuManager.instance) {
const label = document.createElement("span"); ContextMenuManager.instance = new ContextMenuManager();
label.textContent = item.label; }
contentContainer.appendChild(label); return ContextMenuManager.instance;
menuItem.appendChild(contentContainer);
// 快捷键提示(如果有)
if (item.shortcut) {
const shortcut = document.createElement("span");
shortcut.className = "cm-context-menu-item-shortcut";
shortcut.textContent = item.shortcut;
menuItem.appendChild(shortcut);
} }
// 如果有子菜单,创建或获取子菜单 /**
if (item.submenu && item.submenu.length > 0) { * 初始化事件处理器
// 使用菜单项标签作为子菜单ID */
const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`; private initializeEventHandlers(): void {
const submenu = getOrCreateSubmenu(submenuId); // 点击事件委托
this.menuClickHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
// 清空现有子菜单内容 if (menuItem && menuItem.dataset.command) {
e.preventDefault();
e.stopPropagation();
// 添加点击动画
this.addRippleEffect(menuItem, e);
// 执行命令
const commandName = menuItem.dataset.command;
const command = this.getCommandByName(commandName);
if (command && this.currentView) {
command(this.currentView);
}
// 隐藏菜单
this.hide();
}
};
// 鼠标事件委托
this.menuMouseHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
if (!menuItem) return;
if (e.type === 'mouseenter') {
this.handleMenuItemMouseEnter(menuItem);
} else if (e.type === 'mouseleave') {
this.handleMenuItemMouseLeave(menuItem, e);
}
};
// 键盘事件处理器
this.keyDownHandler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
this.hide();
}
};
// 点击外部关闭处理器
this.clickOutsideHandler = (e: MouseEvent) => {
if (this.menuElement && !this.isClickInsideMenu(e.target as Node)) {
this.hide();
}
};
}
/**
* 获取或创建主菜单元素
*/
private getOrCreateMenuElement(): HTMLElement {
if (!this.menuElement) {
this.menuElement = document.createElement("div");
this.menuElement.className = "cm-context-menu";
this.menuElement.style.display = "none";
document.body.appendChild(this.menuElement);
// 阻止菜单内右键点击冒泡
this.menuElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// 添加事件委托
this.menuElement.addEventListener('click', this.menuClickHandler!);
this.menuElement.addEventListener('mouseenter', this.menuMouseHandler!, true);
this.menuElement.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.menuElement;
}
/**
* 创建或获取子菜单元素
*/
private getOrCreateSubmenu(id: string): HTMLElement {
if (!this.submenuPool.has(id)) {
const submenu = document.createElement("div");
submenu.className = "cm-context-menu cm-context-submenu";
submenu.style.display = "none";
document.body.appendChild(submenu);
this.submenuPool.set(id, submenu);
// 阻止子菜单点击事件冒泡
submenu.addEventListener('click', (e) => {
e.stopPropagation();
});
// 添加事件委托
submenu.addEventListener('click', this.menuClickHandler!);
submenu.addEventListener('mouseenter', this.menuMouseHandler!, true);
submenu.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.submenuPool.get(id)!;
}
/**
* 创建菜单项DOM元素
*/
private createMenuItemElement(item: MenuItem): HTMLElement {
const menuItem = this.menuItemPool.get();
// 如果有子菜单,添加相应类
if (item.submenu && item.submenu.length > 0) {
menuItem.classList.add("cm-context-menu-item-with-submenu");
}
// 创建内容容器
const contentContainer = document.createElement("div");
contentContainer.className = "cm-context-menu-item-label";
// 标签文本
const label = document.createElement("span");
label.textContent = item.label;
contentContainer.appendChild(label);
menuItem.appendChild(contentContainer);
// 快捷键提示(如果有)
if (item.shortcut) {
const shortcut = document.createElement("span");
shortcut.className = "cm-context-menu-item-shortcut";
shortcut.textContent = item.shortcut;
menuItem.appendChild(shortcut);
}
// 存储命令信息用于事件委托
if (item.command) {
menuItem.dataset.command = this.registerCommand(item.command);
}
// 处理子菜单
if (item.submenu && item.submenu.length > 0) {
const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`;
menuItem.dataset.submenuId = submenuId;
const submenu = this.getOrCreateSubmenu(submenuId);
this.populateSubmenu(submenu, item.submenu);
// 记录子菜单
if (!window.cmSubmenus) {
window.cmSubmenus = new Map();
}
window.cmSubmenus.set(submenuId, submenu);
}
return menuItem;
}
/**
* 填充子菜单内容
*/
private populateSubmenu(submenu: HTMLElement, items: MenuItem[]): void {
// 清空现有内容
while (submenu.firstChild) { while (submenu.firstChild) {
submenu.removeChild(submenu.firstChild); submenu.removeChild(submenu.firstChild);
} }
// 添加子菜单项 // 添加子菜单项
item.submenu.forEach(subItem => { items.forEach(item => {
const subMenuItemElement = createMenuItemElement(subItem, view); const subMenuItemElement = this.createMenuItemElement(item);
submenu.appendChild(subMenuItemElement); submenu.appendChild(subMenuItemElement);
}); });
@@ -114,313 +268,318 @@ function createMenuItemElement(item: MenuItem, view: EditorView): HTMLElement {
submenu.style.pointerEvents = 'none'; submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden'; submenu.style.visibility = 'hidden';
submenu.style.display = 'block'; submenu.style.display = 'block';
}
// 当鼠标悬停在菜单项上时,显示子菜单 /**
menuItem.addEventListener('mouseenter', () => { * 命令注册和管理
const rect = menuItem.getBoundingClientRect(); */
private commands: Map<string, (view: EditorView) => void> = new Map();
private commandCounter = 0;
// 计算子菜单位置 private registerCommand(command: (view: EditorView) => void): string {
submenu.style.left = `${rect.right}px`; const commandId = `cmd_${this.commandCounter++}`;
submenu.style.top = `${rect.top}px`; this.commands.set(commandId, command);
return commandId;
}
// 检查子菜单是否会超出屏幕右侧 private getCommandByName(commandId: string): ((view: EditorView) => void) | undefined {
setTimeout(() => { return this.commands.get(commandId);
const submenuRect = submenu.getBoundingClientRect(); }
if (submenuRect.right > window.innerWidth) {
// 如果会超出右侧,则显示在左侧
submenu.style.left = `${rect.left - submenuRect.width}px`;
}
// 检查子菜单是否会超出屏幕底部 /**
if (submenuRect.bottom > window.innerHeight) { * 处理菜单项鼠标进入事件
// 如果会超出底部,则向上调整 */
const newTop = rect.top - (submenuRect.bottom - window.innerHeight); private handleMenuItemMouseEnter(menuItem: HTMLElement): void {
submenu.style.top = `${Math.max(0, newTop)}px`; const submenuId = menuItem.dataset.submenuId;
} if (!submenuId) return;
}, 0);
// 显示子菜单 const submenu = this.submenuPool.get(submenuId);
submenu.style.opacity = '1'; if (!submenu) return;
submenu.style.pointerEvents = 'auto';
submenu.style.visibility = 'visible';
submenu.style.transform = 'translateX(0)';
});
// 当鼠标离开菜单项时,隐藏子菜单 const rect = menuItem.getBoundingClientRect();
menuItem.addEventListener('mouseleave', (e) => {
// 检查是否移动到子菜单 // 计算子菜单位置
const toElement = e.relatedTarget as HTMLElement; submenu.style.left = `${rect.right}px`;
if (submenu.contains(toElement)) { submenu.style.top = `${rect.top}px`;
return; // 如果移动到子菜单上,不隐藏
// 检查子菜单是否会超出屏幕
requestAnimationFrame(() => {
const submenuRect = submenu.getBoundingClientRect();
if (submenuRect.right > window.innerWidth) {
submenu.style.left = `${rect.left - submenuRect.width}px`;
} }
// 隐藏子菜单 if (submenuRect.bottom > window.innerHeight) {
submenu.style.opacity = '0'; const newTop = rect.top - (submenuRect.bottom - window.innerHeight);
submenu.style.pointerEvents = 'none'; submenu.style.top = `${Math.max(0, newTop)}px`;
submenu.style.transform = 'translateX(10px)';
// 延迟设置visibility以便过渡动画能够完成
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
});
// 当鼠标离开子菜单时,隐藏它
submenu.addEventListener('mouseleave', (e) => {
// 检查是否移动回父菜单项
const toElement = e.relatedTarget as HTMLElement;
if (menuItem.contains(toElement)) {
return; // 如果移动回父菜单项,不隐藏
} }
// 隐藏子菜单
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
// 延迟设置visibility以便过渡动画能够完成
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
}); });
// 记录子菜单 // 显示子菜单
if (!window.cmSubmenus) { submenu.style.opacity = '1';
window.cmSubmenus = new Map(); submenu.style.pointerEvents = 'auto';
submenu.style.visibility = 'visible';
submenu.style.transform = 'translateX(0)';
this.activeSubmenus.add(submenu);
}
/**
* 处理菜单项鼠标离开事件
*/
private handleMenuItemMouseLeave(menuItem: HTMLElement, e: MouseEvent): void {
const submenuId = menuItem.dataset.submenuId;
if (!submenuId) return;
const submenu = this.submenuPool.get(submenuId);
if (!submenu) return;
// 检查是否移动到子菜单上
const toElement = e.relatedTarget as HTMLElement;
if (submenu.contains(toElement)) {
return;
} }
window.cmSubmenus.set(submenuId, submenu);
this.hideSubmenu(submenu);
} }
// 点击事件仅当有command时添加 /**
if (item.command) { * 隐藏子菜单
menuItem.addEventListener("click", (e) => { */
e.preventDefault(); private hideSubmenu(submenu: HTMLElement): void {
e.stopPropagation(); submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
// 添加点击动画效果 setTimeout(() => {
const ripple = document.createElement("div"); if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
this.activeSubmenus.delete(submenu);
}
/**
* 添加点击波纹效果
*/
private addRippleEffect(menuItem: HTMLElement, e: MouseEvent): void {
let ripple: HTMLElement;
if (this.ripplePool.length > 0) {
ripple = this.ripplePool.pop()!;
} else {
ripple = document.createElement("div");
ripple.className = "cm-context-menu-item-ripple"; ripple.className = "cm-context-menu-item-ripple";
}
// 计算相对位置 // 计算相对位置
const rect = menuItem.getBoundingClientRect(); const rect = menuItem.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const y = e.clientY - rect.top; const y = e.clientY - rect.top;
ripple.style.left = (x - 50) + "px"; ripple.style.left = (x - 50) + "px";
ripple.style.top = (y - 50) + "px"; ripple.style.top = (y - 50) + "px";
ripple.style.transform = "scale(0)";
ripple.style.opacity = "1";
menuItem.appendChild(ripple); menuItem.appendChild(ripple);
// 执行动画
requestAnimationFrame(() => {
ripple.style.transform = "scale(1)";
ripple.style.opacity = "0";
// 执行点击动画
setTimeout(() => { setTimeout(() => {
ripple.style.transform = "scale(1)"; if (ripple.parentNode === menuItem) {
ripple.style.opacity = "0"; menuItem.removeChild(ripple);
this.ripplePool.push(ripple);
// 动画完成后移除ripple元素 }
setTimeout(() => { }, 300);
if (ripple.parentNode === menuItem) {
menuItem.removeChild(ripple);
}
}, 300);
}, 10);
// 执行命令
item.command!(view);
// 隐藏菜单
hideContextMenu();
}); });
} }
return menuItem; /**
} * 检查点击是否在菜单内
*/
private isClickInsideMenu(target: Node): boolean {
if (this.menuElement && this.menuElement.contains(target)) {
return true;
}
/** // 检查是否在子菜单内
* 创建分隔线 for (const submenu of this.activeSubmenus) {
*/ if (submenu.contains(target)) {
function createDivider(): HTMLElement { return true;
const divider = document.createElement("div"); }
divider.className = "cm-context-menu-divider"; }
return divider;
}
/** return false;
* 添加菜单组
* @param menuElement 菜单元素
* @param title 菜单组标题
* @param items 菜单项
* @param view 编辑器视图
*/
function addMenuGroup(menuElement: HTMLElement, title: string | null, items: MenuItem[], view: EditorView): void {
// 如果有标题,添加组标题
if (title) {
const groupTitle = document.createElement("div");
groupTitle.className = "cm-context-menu-group-title";
groupTitle.textContent = title;
menuElement.appendChild(groupTitle);
} }
// 添加菜单项 /**
items.forEach(item => { * 定位菜单元素
const menuItemElement = createMenuItemElement(item, view); */
menuElement.appendChild(menuItemElement); private positionMenu(menu: HTMLElement, clientX: number, clientY: number): void {
}); const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let left = clientX;
let top = clientY;
requestAnimationFrame(() => {
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
if (left + menuWidth > windowWidth) {
left = windowWidth - menuWidth - 5;
}
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 5;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
});
}
/**
* 显示上下文菜单
*/
show(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
this.currentView = view;
// 获取或创建菜单元素
const menu = this.getOrCreateMenuElement();
// 隐藏所有子菜单
this.hideAllSubmenus();
// 清空现有菜单项并回收到池中
while (menu.firstChild) {
const child = menu.firstChild as HTMLElement;
if (child.classList.contains('cm-context-menu-item')) {
this.menuItemPool.release(child);
}
menu.removeChild(child);
}
// 清空命令注册
this.commands.clear();
this.commandCounter = 0;
// 添加主菜单项
items.forEach(item => {
const menuItemElement = this.createMenuItemElement(item);
menu.appendChild(menuItemElement);
});
// 显示菜单
menu.style.display = "block";
// 定位菜单
this.positionMenu(menu, clientX, clientY);
// 添加全局事件监听器
document.addEventListener("click", this.clickOutsideHandler!, true);
document.addEventListener("keydown", this.keyDownHandler!);
// 触发显示动画
requestAnimationFrame(() => {
if (menu) {
menu.classList.add("show");
}
});
}
/**
* 隐藏所有子菜单
*/
private hideAllSubmenus(): void {
this.activeSubmenus.forEach(submenu => {
this.hideSubmenu(submenu);
});
this.activeSubmenus.clear();
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.transform = 'translateX(10px)';
});
}
}
/**
* 隐藏上下文菜单
*/
hide(): void {
// 隐藏所有子菜单
this.hideAllSubmenus();
if (this.menuElement) {
// 添加淡出动画
this.menuElement.classList.remove("show");
this.menuElement.classList.add("hide");
// 等待动画完成后隐藏
setTimeout(() => {
if (this.menuElement) {
this.menuElement.style.display = "none";
this.menuElement.classList.remove("hide");
}
}, 150);
}
// 移除全局事件监听器
if (this.clickOutsideHandler) {
document.removeEventListener("click", this.clickOutsideHandler, true);
}
if (this.keyDownHandler) {
document.removeEventListener("keydown", this.keyDownHandler);
}
this.currentView = null;
}
/**
* 销毁管理器
*/
destroy(): void {
this.hide();
if (this.menuElement) {
document.body.removeChild(this.menuElement);
this.menuElement = null;
}
this.submenuPool.forEach(submenu => {
if (submenu.parentNode) {
document.body.removeChild(submenu);
}
});
this.submenuPool.clear();
this.menuItemPool.clear();
this.commands.clear();
this.activeSubmenus.clear();
this.ripplePool.length = 0;
if (window.cmSubmenus) {
window.cmSubmenus.clear();
}
}
} }
// 获取单例实例
const contextMenuManager = ContextMenuManager.getInstance();
/** /**
* 显示上下文菜单 * 显示上下文菜单
*/ */
export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void { export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
// 获取或创建菜单元素 contextMenuManager.show(view, clientX, clientY, items);
const menu = getOrCreateMenuElement();
// 如果已经有菜单显示,先隐藏所有子菜单
hideAllSubmenus();
// 清空现有菜单项
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
// 添加主菜单项
items.forEach(item => {
const menuItemElement = createMenuItemElement(item, view);
menu.appendChild(menuItemElement);
});
// 显示菜单
menu.style.display = "block";
// 定位菜单
positionMenu(menu, clientX, clientY);
// 添加点击外部关闭事件
if (clickOutsideHandler) {
document.removeEventListener("click", clickOutsideHandler, true);
}
clickOutsideHandler = (e: MouseEvent) => {
// 检查点击是否在菜单外
if (menu && !menu.contains(e.target as Node)) {
let isInSubmenu = false;
// 检查是否点击在子菜单内
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
if (submenu.contains(e.target as Node)) {
isInSubmenu = true;
}
});
}
if (!isInSubmenu) {
hideContextMenu();
}
}
};
// 使用捕获阶段确保事件被处理
document.addEventListener("click", clickOutsideHandler, true);
// ESC键关闭
document.addEventListener("keydown", handleKeyDown);
// 触发显示动画
setTimeout(() => {
if (menu) {
menu.classList.add("show");
}
}, 10);
}
/**
* 隐藏所有子菜单
*/
function hideAllSubmenus(): void {
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.transform = 'translateX(10px)';
});
}
}
/**
* 处理键盘事件
*/
function handleKeyDown(e: KeyboardEvent): void {
if (e.key === "Escape") {
hideContextMenu();
document.removeEventListener("keydown", handleKeyDown);
}
}
/**
* 隐藏上下文菜单
*/
export function hideContextMenu(): void {
// 隐藏所有子菜单
hideAllSubmenus();
if (menuElement) {
// 添加淡出动画
menuElement.classList.remove("show");
menuElement.classList.add("hide");
// 等待动画完成后隐藏不移除DOM元素
setTimeout(() => {
if (menuElement) {
menuElement.style.display = "none";
menuElement.classList.remove("hide");
}
}, 150);
}
if (clickOutsideHandler) {
document.removeEventListener("click", clickOutsideHandler, true);
clickOutsideHandler = null;
}
document.removeEventListener("keydown", handleKeyDown);
}
/**
* 定位菜单元素
*/
function positionMenu(menu: HTMLElement, clientX: number, clientY: number): void {
// 获取窗口尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 初始位置设置
let left = clientX;
let top = clientY;
// 确保菜单在视窗内
setTimeout(() => {
// 计算菜单尺寸
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
// 确保菜单不会超出右侧边界
if (left + menuWidth > windowWidth) {
left = windowWidth - menuWidth - 5;
}
// 确保菜单不会超出底部边界
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 5;
}
// 应用位置
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
}, 0);
} }

View File

@@ -11,10 +11,6 @@ import { useKeybindingStore } from "@/stores/keybindingStore";
import { import {
undo, redo undo, redo
} from "@codemirror/commands"; } from "@codemirror/commands";
import {
deleteBlock, formatCurrentBlock,
addNewBlockAfterCurrent, addNewBlockAfterLast, addNewBlockBeforeCurrent
} from "../extensions/codeblock/commands";
import i18n from "@/i18n"; import i18n from "@/i18n";
import {useSystemStore} from "@/stores/systemStore"; import {useSystemStore} from "@/stores/systemStore";
@@ -133,44 +129,6 @@ function createHistoryItems(): MenuItem[] {
]; ];
} }
/**
* 创建代码块相关菜单项
*/
function createCodeBlockItems(): MenuItem[] {
const defaultOptions = { defaultBlockToken: 'text', defaultBlockAutoDetect: true };
return [
// 格式化
{
label: t("keybindings.commands.blockFormat"),
command: formatCurrentBlock,
shortcut: getShortcutText(KeyBindingCommand.BlockFormatCommand)
},
// 删除
{
label: t("keybindings.commands.blockDelete"),
command: deleteBlock(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockDeleteCommand)
},
// 在当前块后添加新块
{
label: t("keybindings.commands.blockAddAfterCurrent"),
command: addNewBlockAfterCurrent(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterCurrentCommand)
},
// 在当前块前添加新块
{
label: t("keybindings.commands.blockAddBeforeCurrent"),
command: addNewBlockBeforeCurrent(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddBeforeCurrentCommand)
},
// 在最后添加新块
{
label: t("keybindings.commands.blockAddAfterLast"),
command: addNewBlockAfterLast(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterLastCommand)
}
];
}
/** /**
* 创建主菜单项 * 创建主菜单项
@@ -185,11 +143,7 @@ function createMainMenuItems(): MenuItem[] {
// 构建主菜单 // 构建主菜单
return [ return [
...basicItems, ...basicItems,
...historyItems, ...historyItems
{
label: t("extensions.codeblock.name"),
submenu: createCodeBlockItems()
}
]; ];
} }

View File

@@ -17,7 +17,7 @@ const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-
/** /**
* 获取被复制的范围和内容 * 获取被复制的范围和内容
*/ */
function copiedRange(state: EditorState) { function copiedRange(state: EditorState, forCut: boolean = false) {
const content: string[] = []; const content: string[] = [];
const ranges: any[] = []; const ranges: any[] = [];
@@ -37,7 +37,13 @@ function copiedRange(state: EditorState) {
const lineContent = state.sliceDoc(line.from, line.to); const lineContent = state.sliceDoc(line.from, line.to);
if (!copiedLines.includes(line.from)) { if (!copiedLines.includes(line.from)) {
content.push(lineContent); content.push(lineContent);
ranges.push(range); // 对于剪切操作,需要包含整行范围(包括换行符)
if (forCut) {
const lineEnd = line.to < state.doc.length ? line.to + 1 : line.to;
ranges.push({ from: line.from, to: lineEnd });
} else {
ranges.push(range);
}
copiedLines.push(line.from); copiedLines.push(line.from);
} }
} }
@@ -68,7 +74,7 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({
}, },
cut(event, view) { cut(event, view) {
let { text, ranges } = copiedRange(view.state); let { text, ranges } = copiedRange(view.state, true);
// 将块分隔符替换为双换行符 // 将块分隔符替换为双换行符
text = text.replaceAll(blockSeparatorRegex, "\n\n"); text = text.replaceAll(blockSeparatorRegex, "\n\n");
@@ -93,7 +99,7 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({
* 复制和剪切的通用函数 * 复制和剪切的通用函数
*/ */
const copyCut = (view: EditorView, cut: boolean): boolean => { const copyCut = (view: EditorView, cut: boolean): boolean => {
let { text, ranges } = copiedRange(view.state); let { text, ranges } = copiedRange(view.state, cut);
// 将块分隔符替换为双换行符 // 将块分隔符替换为双换行符
text = text.replaceAll(blockSeparatorRegex, "\n\n"); text = text.replaceAll(blockSeparatorRegex, "\n\n");

View File

@@ -66,7 +66,7 @@ const defaultEditorOptions = {
* *
* command字段映射到具体的前端方法和翻译键 * command字段映射到具体的前端方法和翻译键
*/ */
export const commandRegistry = { export const commands = {
[KeyBindingCommand.ShowSearchCommand]: { [KeyBindingCommand.ShowSearchCommand]: {
handler: showSearchVisibilityCommand, handler: showSearchVisibilityCommand,
descriptionKey: 'keybindings.commands.showSearch' descriptionKey: 'keybindings.commands.showSearch'
@@ -299,7 +299,7 @@ export const commandRegistry = {
* @returns undefined * @returns undefined
*/ */
export const getCommandHandler = (command: KeyBindingCommand) => { export const getCommandHandler = (command: KeyBindingCommand) => {
return commandRegistry[command]?.handler; return commands[command]?.handler;
}; };
/** /**
@@ -308,7 +308,7 @@ export const getCommandHandler = (command: KeyBindingCommand) => {
* @returns undefined * @returns undefined
*/ */
export const getCommandDescription = (command: KeyBindingCommand) => { export const getCommandDescription = (command: KeyBindingCommand) => {
const descriptionKey = commandRegistry[command]?.descriptionKey; const descriptionKey = commands[command]?.descriptionKey;
return descriptionKey ? i18n.global.t(descriptionKey) : undefined; return descriptionKey ? i18n.global.t(descriptionKey) : undefined;
}; };
@@ -318,7 +318,7 @@ export const getCommandDescription = (command: KeyBindingCommand) => {
* @returns * @returns
*/ */
export const isCommandRegistered = (command: KeyBindingCommand): boolean => { export const isCommandRegistered = (command: KeyBindingCommand): boolean => {
return command in commandRegistry; return command in commands;
}; };
/** /**
@@ -326,5 +326,5 @@ export const isCommandRegistered = (command: KeyBindingCommand): boolean => {
* @returns * @returns
*/ */
export const getRegisteredCommands = (): KeyBindingCommand[] => { export const getRegisteredCommands = (): KeyBindingCommand[] => {
return Object.keys(commandRegistry) as KeyBindingCommand[]; return Object.keys(commands) as KeyBindingCommand[];
}; };

View File

@@ -2,11 +2,9 @@ import { Extension } from '@codemirror/state';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { KeymapManager } from './keymapManager'; import { KeymapManager } from './keymapManager';
import { ExtensionID } from '@/../bindings/voidraft/internal/models/models';
/** /**
* 异步创建快捷键扩展 * 异步创建快捷键扩展
* 确保快捷键配置和扩展配置已加载
*/ */
export const createDynamicKeymapExtension = async (): Promise<Extension> => { export const createDynamicKeymapExtension = async (): Promise<Extension> => {
const keybindingStore = useKeybindingStore(); const keybindingStore = useKeybindingStore();
@@ -42,17 +40,7 @@ export const updateKeymapExtension = (view: any): void => {
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
}; };
/**
* 获取指定扩展的快捷键
* @param extensionId 扩展ID
* @returns 该扩展的快捷键列表
*/
export const getExtensionKeyBindings = (extensionId: ExtensionID) => {
const keybindingStore = useKeybindingStore();
return keybindingStore.getKeyBindingsByExtension(extensionId);
};
// 导出相关模块 // 导出相关模块
export { KeymapManager } from './keymapManager'; export { KeymapManager } from './keymapManager';
export { commandRegistry, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commandRegistry'; export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';

View File

@@ -2,7 +2,7 @@ import {keymap} from '@codemirror/view';
import {Extension, Compartment} from '@codemirror/state'; import {Extension, Compartment} from '@codemirror/state';
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {KeyBinding, KeymapResult} from './types'; import {KeyBinding, KeymapResult} from './types';
import {getCommandHandler, isCommandRegistered} from './commandRegistry'; import {getCommandHandler, isCommandRegistered} from './commands';
/** /**
* 快捷键管理器 * 快捷键管理器
@@ -82,45 +82,4 @@ export class KeymapManager {
effects: this.compartment.reconfigure(keymap.of(cmKeyBindings)) effects: this.compartment.reconfigure(keymap.of(cmKeyBindings))
}); });
} }
/**
* 按扩展分组快捷键
* @param keyBindings 快捷键配置列表
* @returns 按扩展分组的快捷键映射
*/
static groupByExtension(keyBindings: KeyBindingConfig[]): Map<ExtensionID, KeyBindingConfig[]> {
const groups = new Map<ExtensionID, KeyBindingConfig[]>();
for (const binding of keyBindings) {
if (!groups.has(binding.extension)) {
groups.set(binding.extension, []);
}
groups.get(binding.extension)!.push(binding);
}
return groups;
}
/**
* 验证快捷键配置
* @param keyBindings 快捷键配置列表
* @returns 验证结果
*/
static validateKeyBindings(keyBindings: KeyBindingConfig[]): {
valid: KeyBindingConfig[]
invalid: KeyBindingConfig[]
} {
const valid: KeyBindingConfig[] = [];
const invalid: KeyBindingConfig[] = [];
for (const binding of keyBindings) {
if (binding.enabled && binding.key && isCommandRegistered(binding.command)) {
valid.push(binding);
} else {
invalid.push(binding);
}
}
return {valid, invalid};
}
} }

View File

@@ -1,53 +1,10 @@
import {Compartment, Extension, StateEffect} from '@codemirror/state'; import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
import {createDebounce} from '@/common/utils/debounce';
/**
*
*
*/
export interface ExtensionFactory {
/**
*
* @param config
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
*
* @returns
*/
getDefaultConfig(): any
/**
*
* @param config
* @returns
*/
validateConfig?(config: any): boolean
}
/**
*
*/
interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
*
*/
interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}
/** /**
* *
@@ -66,8 +23,11 @@ export class ExtensionManager {
private extensionFactories = new Map<ExtensionID, ExtensionFactory>(); private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
// 防抖处理 // 防抖处理
private debounceTimers = new Map<ExtensionID, number>(); private debouncedUpdateFunctions = new Map<ExtensionID, {
private debounceDelay = 300; // 默认防抖时间为300毫秒 debouncedFn: (enabled: boolean, config: any) => void;
cancel: () => void;
flush: () => void;
}>();
/** /**
* *
@@ -91,6 +51,22 @@ export class ExtensionManager {
extension: [] // 默认为空扩展(禁用状态) extension: [] // 默认为空扩展(禁用状态)
}); });
} }
// 为每个扩展创建防抖函数
if (!this.debouncedUpdateFunctions.has(id)) {
const { debouncedFn, cancel, flush } = createDebounce(
(enabled: boolean, config: any) => {
this.updateExtensionImmediate(id, enabled, config);
},
{ delay: 300 }
);
this.debouncedUpdateFunctions.set(id, {
debouncedFn,
cancel,
flush
});
}
} }
/** /**
@@ -197,18 +173,13 @@ export class ExtensionManager {
* @param config * @param config
*/ */
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void { updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 清除之前的定时器 const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
if (this.debounceTimers.has(id)) { if (debouncedUpdate) {
window.clearTimeout(this.debounceTimers.get(id)); debouncedUpdate.debouncedFn(enabled, config);
} } else {
// 如果没有防抖函数,直接执行
// 设置新的定时器
const timerId = window.setTimeout(() => {
this.updateExtensionImmediate(id, enabled, config); this.updateExtensionImmediate(id, enabled, config);
this.debounceTimers.delete(id); }
}, this.debounceDelay);
this.debounceTimers.set(id, timerId);
} }
/** /**
@@ -268,72 +239,6 @@ export class ExtensionManager {
} }
} }
/**
*
* @param updates
*/
updateExtensions(updates: Array<{
id: ExtensionID
enabled: boolean
config: any
}>): void {
// 清除所有相关的防抖定时器
for (const update of updates) {
if (this.debounceTimers.has(update.id)) {
window.clearTimeout(this.debounceTimers.get(update.id));
this.debounceTimers.delete(update.id);
}
}
// 更新所有扩展状态
for (const update of updates) {
// 获取扩展状态
const state = this.extensionStates.get(update.id);
if (!state) continue;
// 获取工厂
const factory = state.factory;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(update.config)) {
continue;
}
try {
// 创建新的扩展实例
const extension = update.enabled ? factory.create(update.config) : [];
// 更新内部状态
state.config = update.config;
state.enabled = update.enabled;
state.extension = extension;
} catch (error) {
console.error(`Failed to update extension ${update.id}:`, error);
}
}
// 将更改应用到所有视图
for (const viewInfo of this.viewsMap.values()) {
if (!viewInfo.registered) continue;
const effects: StateEffect<any>[] = [];
for (const update of updates) {
const state = this.extensionStates.get(update.id);
if (!state) continue;
effects.push(state.compartment.reconfigure(state.extension));
}
if (effects.length > 0) {
try {
viewInfo.view.dispatch({ effects });
} catch (error) {
console.error(`Failed to apply extensions to document ${viewInfo.documentId}:`, error);
}
}
}
}
/** /**
* *
@@ -380,11 +285,11 @@ export class ExtensionManager {
* *
*/ */
destroy(): void { destroy(): void {
// 清除所有防抖定时器 // 清除所有防抖函数
for (const timerId of this.debounceTimers.values()) { for (const { cancel } of this.debouncedUpdateFunctions.values()) {
window.clearTimeout(timerId); cancel();
} }
this.debounceTimers.clear(); this.debouncedUpdateFunctions.clear();
this.viewsMap.clear(); this.viewsMap.clear();
this.activeViewId = null; this.activeViewId = null;

View File

@@ -1,6 +1,7 @@
import {ExtensionFactory, ExtensionManager} from './ExtensionManager'; import {ExtensionManager} from './extensionManager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n'; import i18n from '@/i18n';
import {ExtensionFactory} from './types'
// 导入现有扩展的创建函数 // 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';

View File

@@ -1,8 +1,8 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionManager} from './ExtensionManager'; import {ExtensionManager} from './extensionManager';
import {registerAllExtensions} from './factories'; import {registerAllExtensions} from './extensions';
/** /**
* 全局扩展管理器实例 * 全局扩展管理器实例
@@ -58,6 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
}; };
// 导出相关模块 // 导出相关模块
export {ExtensionManager} from './ExtensionManager'; export {ExtensionManager} from './extensionManager';
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './factories'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';
export type {ExtensionFactory} from './ExtensionManager';

View File

@@ -0,0 +1,49 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
/**
* 扩展工厂接口
* 每个扩展需要实现此接口来创建和配置扩展
*/
export interface ExtensionFactory {
/**
* 创建扩展实例
* @param config 扩展配置
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig(): any
/**
* 验证配置
* @param config 配置对象
* @returns 是否有效
*/
validateConfig?(config: any): boolean
}
/**
* 扩展状态
*/
export interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
* 视图信息
*/
export interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}

View File

@@ -11,7 +11,7 @@ import {
getExtensionDescription, getExtensionDescription,
getExtensionDisplayName, getExtensionDisplayName,
hasExtensionConfig hasExtensionConfig
} from '@/views/editor/manager/factories'; } from '@/views/editor/manager/extensions';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue';

View File

@@ -5,7 +5,7 @@ import SettingSection from '../components/SettingSection.vue';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { useSystemStore } from '@/stores/systemStore'; import { useSystemStore } from '@/stores/systemStore';
import { getCommandDescription } from '@/views/editor/keymap/commandRegistry'; import { getCommandDescription } from '@/views/editor/keymap/commands';
import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models"; import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models";
const { t } = useI18n(); const { t } = useI18n();
@@ -61,7 +61,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
if (systemStore.isMacOS) { if (systemStore.isMacOS) {
return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[ return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[
} else { } else {
return ['Ctrl', '', '[']; // Windows/Linux: Ctrl+Shift+[ return ['Ctrl', 'Shift', '[']; // Windows/Linux: Ctrl+Shift+[
} }
} }
@@ -69,7 +69,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
if (systemStore.isMacOS) { if (systemStore.isMacOS) {
return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+] return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+]
} else { } else {
return ['Ctrl', '', ']']; // Windows/Linux: Ctrl+Shift+] return ['Ctrl', 'Shift', ']']; // Windows/Linux: Ctrl+Shift+]
} }
} }
@@ -142,7 +142,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
if (systemStore.isMacOS) { if (systemStore.isMacOS) {
return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\ return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\
} else { } else {
return ['', 'Ctrl', '\\']; // Windows/Linux: Shift+Ctrl+\ return ['Shift', 'Ctrl', '\\']; // Windows/Linux: Shift+Ctrl+\
} }
} }