vue-shiki-monaco
Version:
一个封shiki和monaco-editor的Vue组件
488 lines (487 loc) • 13.4 kB
JavaScript
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