UNPKG

vue-shiki-monaco

Version:
488 lines (487 loc) 13.4 kB
import { ref, reactive, onMounted, onUnmounted, computed } from "vue"; import * as monaco from "monaco-editor-core"; class ContextMenuCore { state; options; listeners = {}; constructor(options) { this.options = options; this.state = { isVisible: false, position: { x: 0, y: 0, direction: "down" }, items: options.items || [] }; this.bindEvents(); } bindEvents() { document.addEventListener("click", this.handleClickOutside); document.addEventListener("contextmenu", this.handleClickOutside); document.addEventListener("keydown", this.handleKeyDown); } unbindEvents() { document.removeEventListener("click", this.handleClickOutside); document.removeEventListener("contextmenu", this.handleClickOutside); document.removeEventListener("keydown", this.handleKeyDown); } handleClickOutside = (_event) => { if (this.state.isVisible) { this.hide(); } }; handleKeyDown = (event) => { if (event.key === "Escape" && this.state.isVisible) { this.hide(); } }; updateState(updates) { this.state = { ...this.state, ...updates }; this.listeners.onStateChange?.(this.state); } show = (event, menuItems, target) => { const targetElement = this.resolveTarget(event, target); if (!targetElement) return; event.preventDefault(); event.stopPropagation(); const position = this.calculatePosition(event, menuItems, targetElement); this.updateState({ isVisible: true, position, items: menuItems }); this.options.onShow?.(); }; hide = () => { this.updateState({ isVisible: false }); this.options.onHide?.(); }; handleItemClick = (item) => { if (!item.disabled) { item.action(); this.hide(); } }; updateTarget = (target) => { this.options.target = target; }; getState = () => ({ ...this.state }); onStateChange = (callback) => { this.listeners.onStateChange = callback; }; destroy = () => { this.unbindEvents(); this.listeners = {}; }; resolveTarget(event, target) { if (target) { return target; } if (this.options.target instanceof HTMLDivElement) { return this.options.target; } if (typeof this.options.target === "string") { return document.querySelector(this.options.target); } let currentElement = event.target; while (currentElement && currentElement !== document.body) { if (currentElement.classList.contains("monaco-editor") || currentElement.classList.contains("monaco-diff-editor")) { return currentElement; } currentElement = currentElement.parentElement; } return document.querySelector(".monaco-editor"); } calculatePosition(event, menuItems, targetElement) { const menuWidth = 200; const menuItemHeight = 28; const separatorHeight = 6; let menuHeight = 8; menuItems.forEach((item) => { if (item.type === "separator") { menuHeight += separatorHeight; } else { menuHeight += menuItemHeight; } }); const rect = targetElement.getBoundingClientRect(); const containerTop = rect.top; const containerLeft = rect.left; const containerWidth = rect.width; const containerHeight = rect.height; const mouseX = event.clientX; const mouseY = event.clientY; const horizontalMargin = 10; const verticalMargin = 20; let direction = "down"; let x = mouseX; let y = mouseY; if (mouseY - containerTop > containerHeight / 2) { direction = "up"; y = mouseY - menuHeight; } else { direction = "down"; y = mouseY; } if (x + menuWidth > containerLeft + containerWidth - horizontalMargin) { x = containerLeft + containerWidth - menuWidth - horizontalMargin; } if (x < containerLeft + horizontalMargin) { x = containerLeft + horizontalMargin; } const maxY = containerTop + containerHeight - menuHeight - verticalMargin; const minY = containerTop + verticalMargin; y = Math.max(minY, Math.min(y, maxY)); return { x, y, direction }; } } function useContextMenu(options) { const isVisible = ref(false); const position = reactive({ x: 0, y: 0, direction: "down" }); const items = ref(options.items || []); let contextMenuCore; onMounted(() => { contextMenuCore = new ContextMenuCore(options); contextMenuCore.onStateChange((state) => { isVisible.value = state.isVisible; position.x = state.position.x; position.y = state.position.y; position.direction = state.position.direction || "down"; items.value = state.items; }); }); onUnmounted(() => { contextMenuCore?.destroy(); }); const show = (event, menuItems, target) => { contextMenuCore?.show(event, menuItems, target); }; const hide = () => { contextMenuCore?.hide(); }; const handleItemClick = (item) => { contextMenuCore?.handleItemClick(item); }; const updateTarget = (target) => { contextMenuCore?.updateTarget(target); }; return { isVisible, position, items: computed(() => items.value), show, hide, handleItemClick, updateTarget }; } const DEFAULT_FONT_SIZE = 16; const MIN_FONT_SIZE = 8; const MAX_FONT_SIZE = 40; function createEditorContextMenu(options) { const { editor, enabledItems, customItems = [] } = options; const defaultItems = [ { type: "item", id: "copy", label: "复制", shortcut: "Ctrl+C", action: async () => { try { editor.trigger("source", "editor.action.clipboardCopyAction", null); } catch (error) { } } }, { type: "item", id: "cut", label: "剪切", shortcut: "Ctrl+X", action: async () => { try { const selection = editor.getSelection(); const model = editor.getModel(); if (!selection || !model) return; const text = model.getValueInRange(selection); await navigator.clipboard.writeText(text); editor.executeEdits("cut", [ { range: selection, text: "", forceMoveMarkers: true } ]); } catch (error) { } } }, { type: "item", id: "paste", label: "粘贴", shortcut: "Ctrl+V", action: async () => { try { const text = await navigator.clipboard.readText(); const selection = editor.getSelection(); editor.executeEdits("paste", [ { range: selection, text, forceMoveMarkers: true } ]); } catch (error) { } } }, { type: "separator" }, { type: "item", id: "selectAll", label: "全选", shortcut: "Ctrl+A", action: () => { const model = editor.getModel(); if (model) { const fullRange = model.getFullModelRange(); editor.setSelection(fullRange); } } }, { type: "separator" }, { type: "item", id: "undo", label: "撤销", shortcut: "Ctrl+Z", action: () => { editor.trigger("context-menu", "undo", null); } }, { type: "item", id: "redo", label: "重做", shortcut: "Ctrl+Y", action: () => { editor.trigger("context-menu", "redo", null); } }, { type: "separator" }, { type: "item", id: "toggleMinimap", label: "切换缩略图", action: () => { const currentOptions = editor.getOptions(); const minimapEnabled = currentOptions.get( monaco.editor.EditorOption.minimap )?.enabled; editor.updateOptions({ minimap: { enabled: !minimapEnabled } }); } }, { type: "item", id: "increaseFontSize", label: "放大字体", shortcut: "Ctrl+=", action: () => { const currentOptions = editor.getOptions(); const currentFontSize = currentOptions.get(monaco.editor.EditorOption.fontSize) || DEFAULT_FONT_SIZE; const newFontSize = Math.min(currentFontSize + 1, MAX_FONT_SIZE); editor.updateOptions({ fontSize: newFontSize }); } }, { type: "item", id: "decreaseFontSize", label: "缩小字体", shortcut: "Ctrl+-", action: () => { const currentOptions = editor.getOptions(); const currentFontSize = currentOptions.get(monaco.editor.EditorOption.fontSize) || DEFAULT_FONT_SIZE; const newFontSize = Math.max(currentFontSize - 1, MIN_FONT_SIZE); editor.updateOptions({ fontSize: newFontSize }); } }, { type: "item", id: "resetFontSize", label: "重置字体大小", shortcut: "Ctrl+0", action: () => { editor.updateOptions({ fontSize: DEFAULT_FONT_SIZE }); } }, { type: "separator" }, { type: "item", id: "format", label: "格式化代码(需自行实现)", shortcut: "Shift+Alt+F", disabled: true, action: () => { editor.trigger("source", "editor.action.formatDocument", null); } }, { type: "item", id: "find", label: "查找", shortcut: "Ctrl+F", action: () => { editor.getAction("actions.find")?.run(); } }, { type: "item", id: "replace", label: "替换", shortcut: "Ctrl+H", action: () => { editor.getAction("editor.action.startFindReplaceAction")?.run(); } } ]; let filteredItems = defaultItems; if (enabledItems && enabledItems.length > 0) { filteredItems = defaultItems.filter( (item) => item.type === "separator" || enabledItems.includes(item.id) ); } if (customItems.length > 0) { if (filteredItems.length > 0) { filteredItems.push({ type: "separator" }); } filteredItems.push(...customItems); } return cleanupSeparators(filteredItems); } function createMinimapContextMenu(options) { const { editor, enabledItems, customItems = [] } = options; const minimapItems = [ { type: "item", id: "toggleMinimap", label: "隐藏缩略图", action: () => { const currentOptions = editor.getOptions(); const minimapEnabled = currentOptions.get( monaco.editor.EditorOption.minimap )?.enabled; editor.updateOptions({ minimap: { enabled: !minimapEnabled } }); } }, { type: "separator" }, { type: "item", id: "minimapSide", label: "切换缩略图位置", action: () => { const currentOptions = editor.getOptions(); const currentSide = currentOptions.get(monaco.editor.EditorOption.minimap)?.side || "right"; const newSide = currentSide === "right" ? "left" : "right"; editor.updateOptions({ minimap: { enabled: true, side: newSide } }); } }, { type: "item", id: "minimapShowSlider", label: "滑块显示", action: () => { const currentOptions = editor.getOptions(); const showSlider = currentOptions.get( monaco.editor.EditorOption.minimap )?.showSlider; const newShowSlider = showSlider === "always" ? "mouseover" : "always"; editor.updateOptions({ minimap: { enabled: true, showSlider: newShowSlider } }); } } ]; let filteredItems = minimapItems; if (enabledItems && enabledItems.length > 0) { filteredItems = minimapItems.filter( (item) => item.type === "separator" || enabledItems.includes(item.id) ); } if (customItems.length > 0) { if (filteredItems.length > 0) { filteredItems.push({ type: "separator" }); } filteredItems.push(...customItems); } return cleanupSeparators(filteredItems); } function cleanupSeparators(items) { const result = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item?.type === "separator" && result.length === 0) { continue; } if (item?.type === "separator" && result[result.length - 1]?.type === "separator") { continue; } item && result.push(item); } while (result.length > 0 && result[result.length - 1]?.type === "separator") { result.pop(); } return result; } const MENU_PRESETS = { minimal: ["copy", "paste", "selectAll"], basic: ["copy", "cut", "paste", "selectAll", "undo", "redo", "toggleMinimap"], full: [ "copy", "cut", "paste", "selectAll", "undo", "redo", "toggleMinimap", "increaseFontSize", "decreaseFontSize", "resetFontSize", "format", "find", "replace" ] }; const MINIMAP_MENU_PRESETS = { minimal: ["toggleMinimap"], basic: ["toggleMinimap", "minimapSide", "minimapShowSlider"], full: ["toggleMinimap", "minimapSide", "minimapShowSlider"] }; export { ContextMenuCore as C, DEFAULT_FONT_SIZE as D, MENU_PRESETS as M, createMinimapContextMenu as a, MINIMAP_MENU_PRESETS as b, createEditorContextMenu as c, MIN_FONT_SIZE as d, MAX_FONT_SIZE as e, useContextMenu as u }; //# sourceMappingURL=editorMenu-BFs5QkAV.js.map