diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 51318e68d1..a84aea9c5b 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -153,7 +153,7 @@ "extensions/search", "extensions/shape-container", "extensions/shape-textbox", - "extensions/slash-menu", + "extensions/context-menu", "extensions/strike", "extensions/structured-content", "extensions/tab", @@ -391,6 +391,10 @@ "source": "/modules/collaboration/self-hosted/overview", "destination": "/guides/collaboration/self-hosted-overview" }, + { + "source": "/extensions/slash-menu", + "destination": "/extensions/context-menu" + }, { "source": "/guides/breaking-changes-v1", "destination": "/guides/migration/breaking-changes-v1" diff --git a/apps/docs/extensions/context-menu.mdx b/apps/docs/extensions/context-menu.mdx new file mode 100644 index 0000000000..73612e860f --- /dev/null +++ b/apps/docs/extensions/context-menu.mdx @@ -0,0 +1,16 @@ +--- +title: ContextMenu extension +sidebarTitle: "Context Menu" +keywords: "ContextMenu extension, superdoc ContextMenu, word ContextMenu, document ContextMenu, docx ContextMenu" +--- + +import Description from '/snippets/extensions/context-menu.mdx' + + + + +## Source code + +import { SourceCodeLink } from '/snippets/components/source-code-link.jsx' + + diff --git a/apps/docs/extensions/slash-menu.mdx b/apps/docs/extensions/slash-menu.mdx deleted file mode 100644 index 699da0a653..0000000000 --- a/apps/docs/extensions/slash-menu.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: SlashMenu extension -sidebarTitle: "Slash Menu" -keywords: "SlashMenu extension, superdoc SlashMenu, word SlashMenu, document SlashMenu, docx SlashMenu" ---- - -import Description from '/snippets/extensions/slash-menu.mdx' - - - - -## Source code - -import { SourceCodeLink } from '/snippets/components/source-code-link.jsx' - - diff --git a/apps/docs/modules/context-menu.mdx b/apps/docs/modules/context-menu.mdx index 26675f0bae..96dd7ed8fe 100644 --- a/apps/docs/modules/context-menu.mdx +++ b/apps/docs/modules/context-menu.mdx @@ -24,7 +24,7 @@ new SuperDoc({ new SuperDoc({ selector: '#editor', modules: { - slashMenu: { + contextMenu: { includeDefaultItems: true, customItems: [], menuProvider: null @@ -37,15 +37,15 @@ new SuperDoc({ Top-level option to disable the context menu entirely - + Whether to include the built-in menu items - + Custom menu sections to add or merge. See [Custom Items](#custom-items). - + Advanced: function to fully control menu contents. See [Menu Provider](#menu-provider). @@ -72,7 +72,7 @@ Add custom items by defining sections in `customItems`. Each section has an `id` ```javascript modules: { - slashMenu: { + contextMenu: { customItems: [{ id: 'my-actions', items: [ @@ -129,7 +129,7 @@ If a custom section has the same `id` as a default section, the items are merged ```javascript modules: { - slashMenu: { + contextMenu: { customItems: [{ // Adds items to the existing 'general' section id: 'general', @@ -151,7 +151,7 @@ For full control over menu contents, use `menuProvider`. It receives the context ```javascript modules: { - slashMenu: { + contextMenu: { menuProvider: (context, sections) => { // Filter out clipboard section in editing mode if (context.documentMode === 'editing') { diff --git a/apps/docs/modules/overview.mdx b/apps/docs/modules/overview.mdx index 68ceb04051..4517873e54 100644 --- a/apps/docs/modules/overview.mdx +++ b/apps/docs/modules/overview.mdx @@ -14,7 +14,7 @@ const superdoc = new SuperDoc({ toolbar: { selector: '#toolbar' }, comments: { allowResolve: true }, collaboration: { ydoc, provider }, - slashMenu: { includeDefaultItems: true } + contextMenu: { includeDefaultItems: true } } }); ``` diff --git a/apps/docs/scripts/sync-sdk-docs.js b/apps/docs/scripts/sync-sdk-docs.js index 9488f1f1db..0a5bbcf3c1 100644 --- a/apps/docs/scripts/sync-sdk-docs.js +++ b/apps/docs/scripts/sync-sdk-docs.js @@ -40,7 +40,7 @@ const SUPPORTED = [ 'search', 'shape-container', 'shape-textbox', - 'slash-menu', + 'context-menu', 'strike', 'structured-content', // contains document-section 'tab', diff --git a/apps/docs/snippets/extensions/slash-menu.mdx b/apps/docs/snippets/extensions/context-menu.mdx similarity index 100% rename from apps/docs/snippets/extensions/slash-menu.mdx rename to apps/docs/snippets/extensions/context-menu.mdx diff --git a/packages/super-editor/src/components/SuperEditor.test.js b/packages/super-editor/src/components/SuperEditor.test.js index 0e013c0488..de3fbf6939 100644 --- a/packages/super-editor/src/components/SuperEditor.test.js +++ b/packages/super-editor/src/components/SuperEditor.test.js @@ -43,8 +43,8 @@ vi.mock('./cursor-helpers.js', () => ({ checkNodeSpecificClicks: checkNodeSpecificClicksMock, })); -vi.mock('./slash-menu/SlashMenu.vue', () => ({ - default: { name: 'SlashMenu', render: () => null }, +vi.mock('./context-menu/ContextMenu.vue', () => ({ + default: { name: 'ContextMenu', render: () => null }, })); vi.mock('./rulers/Ruler.vue', () => ({ diff --git a/packages/super-editor/src/components/SuperEditor.vue b/packages/super-editor/src/components/SuperEditor.vue index d41db8a5a6..21f2f6fbf6 100644 --- a/packages/super-editor/src/components/SuperEditor.vue +++ b/packages/super-editor/src/components/SuperEditor.vue @@ -5,7 +5,7 @@ import { ref, onMounted, onBeforeUnmount, shallowRef, reactive, markRaw, compute import { Editor } from '@superdoc/super-editor'; import { PresentationEditor } from '@core/presentation-editor/index.js'; import { getStarterExtensions } from '@extensions/index.js'; -import SlashMenu from './slash-menu/SlashMenu.vue'; +import ContextMenu from './context-menu/ContextMenu.vue'; import { onMarginClickCursorChange } from './cursor-helpers.js'; import Ruler from './rulers/Ruler.vue'; import GenericPopover from './popovers/GenericPopover.vue'; @@ -987,7 +987,7 @@ const handleMarginClick = (event) => { if (target?.classList?.contains('ProseMirror')) return; // Causes issues with node selection. - if (target?.closest?.('.presentation-editor, .superdoc-layout, .slash-menu')) { + if (target?.closest?.('.presentation-editor, .superdoc-layout, .context-menu')) { return; } @@ -1075,8 +1075,8 @@ onBeforeUnmount(() => { @mouseleave="handleOverlayHide" > - - + import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed, markRaw } from 'vue'; -import { SlashMenuPluginKey } from '../../extensions/slash-menu/slash-menu.js'; +import { ContextMenuPluginKey } from '../../extensions/context-menu/context-menu.js'; import { getPropsByItemId } from './utils.js'; import { shouldBypassContextMenu } from '../../utils/contextmenu-helpers.js'; import { moveCursorToMouseEvent } from '../cursor-helpers.js'; import { getEditorSurfaceElement } from '../../core/helpers/editorSurface.js'; import { getItems } from './menuItems.js'; import { getEditorContext } from './utils.js'; -import { SLASH_MENU_HANDLED_FLAG } from './event-flags.js'; +import { CONTEXT_MENU_HANDLED_FLAG } from './event-flags.js'; import { isMacOS } from '../../core/utilities/isMacOS.js'; const props = defineProps({ @@ -128,11 +128,11 @@ const defaultRender = (context) => { // Access item from the refData or context const item = context.item || context.currentItem; const container = document.createElement('div'); - container.className = 'slash-menu-default-content'; + container.className = 'context-menu-default-content'; if (item.icon) { const iconSpan = document.createElement('span'); - iconSpan.className = 'slash-menu-item-icon'; + iconSpan.className = 'context-menu-item-icon'; iconSpan.innerHTML = item.icon; container.appendChild(iconSpan); } @@ -168,7 +168,7 @@ const renderCustomItem = async (itemId) => { element.hasCustomContent = true; } } catch (error) { - console.warn(`[SlashMenu] Error rendering custom item ${itemId}:`, error); + console.warn(`[ContextMenu] Error rendering custom item ${itemId}:`, error); // Fallback to default rendering const fallbackElement = defaultRender({ ...(currentContext.value || {}), currentItem: item }); element.innerHTML = ''; @@ -252,12 +252,12 @@ const handleGlobalOutsideClick = (event) => { }; /** - * Determines whether the SlashMenu should handle a context menu event. + * Determines whether the ContextMenu should handle a context menu event. * Checks if the editor is editable, context menu is enabled, and the event * should not be bypassed (e.g., modifier keys are not pressed). * * @param {MouseEvent} event - The context menu event to validate - * @returns {boolean} true if the SlashMenu should handle the event, false otherwise + * @returns {boolean} true if the ContextMenu should handle the event, false otherwise */ const shouldHandleContextMenu = (event) => { const readOnly = !props.editor?.isEditable; @@ -268,7 +268,7 @@ const shouldHandleContextMenu = (event) => { }; /** - * Capture phase handler for context menu events that marks the event as handled by SlashMenu. + * Capture phase handler for context menu events that marks the event as handled by ContextMenu. * This flag is used by PresentationInputBridge to skip forwarding the event to the hidden editor, * preventing duplicate context menu handling. * @@ -280,12 +280,12 @@ const shouldHandleContextMenu = (event) => { const handleRightClickCapture = (event) => { try { if (shouldHandleContextMenu(event)) { - event[SLASH_MENU_HANDLED_FLAG] = true; + event[CONTEXT_MENU_HANDLED_FLAG] = true; } } catch (error) { // Prevent handler crashes from breaking the event flow // Log warning but don't throw to allow other handlers to run - console.warn('[SlashMenu] Error in capture phase context menu handler:', error); + console.warn('[ContextMenu] Error in capture phase context menu handler:', error); } }; @@ -325,7 +325,7 @@ const handleRightClick = async (event) => { if (!currentState) return; props.editor.dispatch( - currentState.tr.setMeta(SlashMenuPluginKey, { + currentState.tr.setMeta(ContextMenuPluginKey, { type: 'open', pos: context?.pos ?? currentState.selection.from, clientX: event.clientX, @@ -333,7 +333,7 @@ const handleRightClick = async (event) => { }), ); } catch (error) { - console.error('[SlashMenu] Error opening context menu:', error); + console.error('[ContextMenu] Error opening context menu:', error); } }; @@ -346,7 +346,7 @@ const executeCommand = async (item) => { const menuElement = menuRef.value; const componentProps = getPropsByItemId(item.id, props); - // Convert viewport-relative coordinates (used by fixed-position SlashMenu) + // Convert viewport-relative coordinates (used by fixed-position ContextMenu) // to container-relative coordinates (used by absolute-position GenericPopover) let popoverPosition = { left: menuPosition.value.left, top: menuPosition.value.top }; if (menuElement) { @@ -376,11 +376,11 @@ const closeMenu = (options = { restoreCursor: true }) => { const state = props.editor.state; if (!state) return; // Get plugin state to access anchorPos - const pluginState = SlashMenuPluginKey.getState(state); + const pluginState = ContextMenuPluginKey.getState(state); const anchorPos = pluginState?.anchorPos; // Update prosemirror state to close menu - props.editor.dispatch(state.tr.setMeta(SlashMenuPluginKey, { type: 'close' })); + props.editor.dispatch(state.tr.setMeta(ContextMenuPluginKey, { type: 'close' })); // Restore cursor position and focus only if requested if (options.restoreCursor && anchorPos !== null && anchorPos !== undefined) { @@ -404,8 +404,8 @@ const closeMenu = (options = { restoreCursor: true }) => { * Lifecycle hooks on mount and onBeforeUnmount */ let contextMenuTarget = null; -let slashMenuOpenHandler = null; -let slashMenuCloseHandler = null; +let contextMenuOpenHandler = null; +let contextMenuCloseHandler = null; onMounted(() => { if (!props.editor) return; @@ -420,7 +420,7 @@ onMounted(() => { props.editor.on('update', handleEditorUpdate); // Listen for the slash menu to open - slashMenuOpenHandler = async (event) => { + contextMenuOpenHandler = async (event) => { // Prevent opening the menu in read-only mode const readOnly = !props.editor?.isEditable; if (readOnly) return; @@ -439,7 +439,7 @@ onMounted(() => { selectedId.value = flattenedItems.value[0]?.id || null; } }; - props.editor.on('slashMenu:open', slashMenuOpenHandler); + props.editor.on('contextMenu:open', contextMenuOpenHandler); // Attach context menu to the active surface (flow view.dom or presentation host) contextMenuTarget = getEditorSurfaceElement(props.editor); @@ -448,13 +448,13 @@ onMounted(() => { contextMenuTarget.addEventListener('contextmenu', handleRightClick); } - slashMenuCloseHandler = () => { + contextMenuCloseHandler = () => { cleanupCustomItems(); isOpen.value = false; searchQuery.value = ''; currentContext.value = null; }; - props.editor.on('slashMenu:close', slashMenuCloseHandler); + props.editor.on('contextMenu:close', contextMenuCloseHandler); }); // Cleanup function for event listeners @@ -467,47 +467,51 @@ onBeforeUnmount(() => { if (props.editor) { try { // Remove specific handlers to avoid removing other components' listeners - if (slashMenuOpenHandler) { - props.editor.off('slashMenu:open', slashMenuOpenHandler); + if (contextMenuOpenHandler) { + props.editor.off('contextMenu:open', contextMenuOpenHandler); } - if (slashMenuCloseHandler) { - props.editor.off('slashMenu:close', slashMenuCloseHandler); + if (contextMenuCloseHandler) { + props.editor.off('contextMenu:close', contextMenuCloseHandler); } props.editor.off('update', handleEditorUpdate); contextMenuTarget?.removeEventListener('contextmenu', handleRightClickCapture, true); contextMenuTarget?.removeEventListener('contextmenu', handleRightClick); } catch (error) { - console.warn('[SlashMenu] Error during cleanup:', error); + console.warn('[ContextMenu] Error during cleanup:', error); } } });