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);
}
}
});
-