Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit 633989b

Browse files
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 <caiopizzol@gmail.com>
1 parent 94b12b1 commit 633989b

2 files changed

Lines changed: 103 additions & 8 deletions

File tree

src/index.tsx

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const SuperDocTemplateBuilder = forwardRef<
107107
menu = {},
108108
list = {},
109109
toolbar,
110+
slashMenu,
110111
onReady,
111112
onTrigger,
112113
onFieldInsert,
@@ -228,6 +229,38 @@ const SuperDocTemplateBuilder = forwardRef<
228229
[onFieldInsert, onFieldsChange],
229230
);
230231

232+
const sanitizeFieldAlias = useCallback((alias?: string | null): string | null => {
233+
if (!alias) return null;
234+
let normalized = alias.trim();
235+
if (!normalized) return null;
236+
if (normalized.length > 50) {
237+
normalized = `${normalized.slice(0, 47).trim().replace(/[-_. ]+$/, "")}...`;
238+
}
239+
240+
const collapsedWhitespace = normalized.replace(/[\n\r\t]+/g, " ").replace(/\s+/g, " ");
241+
const safeAlias = collapsedWhitespace.replace(/[^a-zA-Z0-9 _-]/g, "");
242+
return safeAlias.trim().replace(/[-_. ]+$/, "") || null;
243+
}, []);
244+
245+
const ensureUniqueAlias = useCallback(
246+
(alias: string): string => {
247+
const existingAliases = new Set(templateFields.map((field) => field.alias));
248+
249+
if (!existingAliases.has(alias)) return alias;
250+
251+
let counter = 2;
252+
let candidate = `${alias} ${counter}`;
253+
254+
while (existingAliases.has(candidate)) {
255+
counter += 1;
256+
candidate = `${alias} ${counter}`;
257+
}
258+
259+
return candidate;
260+
},
261+
[templateFields],
262+
);
263+
231264
const updateField = useCallback(
232265
(id: string, updates: Partial<Types.TemplateField>): boolean => {
233266
if (!superdocRef.current?.activeEditor) return false;
@@ -451,19 +484,62 @@ const SuperDocTemplateBuilder = forwardRef<
451484
},
452485
};
453486

487+
const cleanSlashMenuItems = slashMenu?.items?.filter((item) => item.id !== "create-field") ?? [];
488+
489+
const createFieldItem: Types.SlashMenuItem = {
490+
id: "create-field",
491+
label: "Create Field",
492+
icon: "🏷️",
493+
showWhen: (context) => context.hasSelection,
494+
action: (editorInstance, context) => {
495+
const activeEditor = editorInstance ?? superdocRef.current?.activeEditor;
496+
if (!activeEditor || activeEditor.state.selection?.empty) return;
497+
498+
const selection = activeEditor.state.selection;
499+
const selectionText = context.selectedText
500+
|| activeEditor.state.doc.textBetween(
501+
selection.from,
502+
selection.to,
503+
"\n",
504+
"\n",
505+
);
506+
507+
const sanitized = sanitizeFieldAlias(selectionText);
508+
if (!sanitized) return;
509+
510+
const alias = ensureUniqueAlias(sanitized);
511+
insertFieldInternal("inline", {
512+
alias,
513+
category: "Custom",
514+
defaultValue: selectionText || alias,
515+
});
516+
},
517+
};
518+
519+
const slashMenuConfig: Types.SlashMenuConfig = {
520+
...(slashMenu || {}),
521+
items: [createFieldItem, ...cleanSlashMenuItems],
522+
};
523+
524+
const modulesConfig: Record<string, any> = {
525+
slashMenu: slashMenuConfig,
526+
};
527+
528+
if (toolbarSettings) {
529+
modulesConfig.toolbar = {
530+
selector: toolbarSettings.selector,
531+
toolbarGroups: toolbarSettings.config.toolbarGroups || ["center"],
532+
excludeItems: toolbarSettings.config.excludeItems || [],
533+
...toolbarSettings.config,
534+
};
535+
}
536+
454537
const instance = new SuperDoc({
455538
...config,
456539
...(toolbarSettings && {
457540
toolbar: toolbarSettings.selector,
458-
modules: {
459-
toolbar: {
460-
selector: toolbarSettings.selector,
461-
toolbarGroups: toolbarSettings.config.toolbarGroups || ["center"],
462-
excludeItems: toolbarSettings.config.excludeItems || [],
463-
...toolbarSettings.config,
464-
},
465-
},
466541
}),
542+
modules: modulesConfig,
467543
});
468544

469545
superdocRef.current = instance;

src/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ export interface TriggerEvent {
2121
cleanup: () => void;
2222
}
2323

24+
export interface SlashMenuContext {
25+
hasSelection: boolean;
26+
selectedText?: string;
27+
}
28+
29+
export interface SlashMenuItem {
30+
id: string;
31+
label: string;
32+
icon?: string;
33+
showWhen?: (context: SlashMenuContext) => boolean;
34+
action: (editor: any | null | undefined, context: SlashMenuContext) => void | Promise<void>;
35+
}
36+
37+
export interface SlashMenuConfig {
38+
items?: SlashMenuItem[];
39+
[key: string]: unknown;
40+
}
41+
2442
export interface FieldMenuProps {
2543
isVisible: boolean;
2644
position?: DOMRect;
@@ -81,6 +99,7 @@ export interface SuperDocTemplateBuilderProps {
8199
menu?: MenuConfig;
82100
list?: ListConfig;
83101
toolbar?: boolean | string | ToolbarConfig;
102+
slashMenu?: SlashMenuConfig;
84103

85104
// Events
86105
onReady?: () => void;

0 commit comments

Comments
 (0)