From 633989b5898e6d56897ad285a0253d272997e7f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Oct 2025 19:47:21 +0000 Subject: [PATCH] feat: Add slash menu for creating fields This commit introduces a new slash menu command to create fields from selected text. It also includes helper functions for sanitizing and ensuring unique field aliases. Co-authored-by: caiopizzol --- src/index.tsx | 92 ++++++++++++++++++++++++++++++++++++++++++++++----- src/types.ts | 19 +++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 197da12..a54b958 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -107,6 +107,7 @@ const SuperDocTemplateBuilder = forwardRef< menu = {}, list = {}, toolbar, + slashMenu, onReady, onTrigger, onFieldInsert, @@ -228,6 +229,38 @@ const SuperDocTemplateBuilder = forwardRef< [onFieldInsert, onFieldsChange], ); + const sanitizeFieldAlias = useCallback((alias?: string | null): string | null => { + if (!alias) return null; + let normalized = alias.trim(); + if (!normalized) return null; + if (normalized.length > 50) { + normalized = `${normalized.slice(0, 47).trim().replace(/[-_. ]+$/, "")}...`; + } + + const collapsedWhitespace = normalized.replace(/[\n\r\t]+/g, " ").replace(/\s+/g, " "); + const safeAlias = collapsedWhitespace.replace(/[^a-zA-Z0-9 _-]/g, ""); + return safeAlias.trim().replace(/[-_. ]+$/, "") || null; + }, []); + + const ensureUniqueAlias = useCallback( + (alias: string): string => { + const existingAliases = new Set(templateFields.map((field) => field.alias)); + + if (!existingAliases.has(alias)) return alias; + + let counter = 2; + let candidate = `${alias} ${counter}`; + + while (existingAliases.has(candidate)) { + counter += 1; + candidate = `${alias} ${counter}`; + } + + return candidate; + }, + [templateFields], + ); + const updateField = useCallback( (id: string, updates: Partial): boolean => { if (!superdocRef.current?.activeEditor) return false; @@ -451,19 +484,62 @@ const SuperDocTemplateBuilder = forwardRef< }, }; + const cleanSlashMenuItems = slashMenu?.items?.filter((item) => item.id !== "create-field") ?? []; + + const createFieldItem: Types.SlashMenuItem = { + id: "create-field", + label: "Create Field", + icon: "🏷️", + showWhen: (context) => context.hasSelection, + action: (editorInstance, context) => { + const activeEditor = editorInstance ?? superdocRef.current?.activeEditor; + if (!activeEditor || activeEditor.state.selection?.empty) return; + + const selection = activeEditor.state.selection; + const selectionText = context.selectedText + || activeEditor.state.doc.textBetween( + selection.from, + selection.to, + "\n", + "\n", + ); + + const sanitized = sanitizeFieldAlias(selectionText); + if (!sanitized) return; + + const alias = ensureUniqueAlias(sanitized); + insertFieldInternal("inline", { + alias, + category: "Custom", + defaultValue: selectionText || alias, + }); + }, + }; + + const slashMenuConfig: Types.SlashMenuConfig = { + ...(slashMenu || {}), + items: [createFieldItem, ...cleanSlashMenuItems], + }; + + const modulesConfig: Record = { + slashMenu: slashMenuConfig, + }; + + if (toolbarSettings) { + modulesConfig.toolbar = { + selector: toolbarSettings.selector, + toolbarGroups: toolbarSettings.config.toolbarGroups || ["center"], + excludeItems: toolbarSettings.config.excludeItems || [], + ...toolbarSettings.config, + }; + } + const instance = new SuperDoc({ ...config, ...(toolbarSettings && { toolbar: toolbarSettings.selector, - modules: { - toolbar: { - selector: toolbarSettings.selector, - toolbarGroups: toolbarSettings.config.toolbarGroups || ["center"], - excludeItems: toolbarSettings.config.excludeItems || [], - ...toolbarSettings.config, - }, - }, }), + modules: modulesConfig, }); superdocRef.current = instance; diff --git a/src/types.ts b/src/types.ts index ebc6915..7602c2c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,24 @@ export interface TriggerEvent { cleanup: () => void; } +export interface SlashMenuContext { + hasSelection: boolean; + selectedText?: string; +} + +export interface SlashMenuItem { + id: string; + label: string; + icon?: string; + showWhen?: (context: SlashMenuContext) => boolean; + action: (editor: any | null | undefined, context: SlashMenuContext) => void | Promise; +} + +export interface SlashMenuConfig { + items?: SlashMenuItem[]; + [key: string]: unknown; +} + export interface FieldMenuProps { isVisible: boolean; position?: DOMRect; @@ -81,6 +99,7 @@ export interface SuperDocTemplateBuilderProps { menu?: MenuConfig; list?: ListConfig; toolbar?: boolean | string | ToolbarConfig; + slashMenu?: SlashMenuConfig; // Events onReady?: () => void;