Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 84 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const SuperDocTemplateBuilder = forwardRef<
menu = {},
list = {},
toolbar,
slashMenu,
onReady,
onTrigger,
onFieldInsert,
Expand Down Expand Up @@ -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<Types.TemplateField>): boolean => {
if (!superdocRef.current?.activeEditor) return false;
Expand Down Expand Up @@ -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<string, any> = {
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;
Expand Down
19 changes: 19 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
}

export interface SlashMenuConfig {
items?: SlashMenuItem[];
[key: string]: unknown;
}

export interface FieldMenuProps {
isVisible: boolean;
position?: DOMRect;
Expand Down Expand Up @@ -81,6 +99,7 @@ export interface SuperDocTemplateBuilderProps {
menu?: MenuConfig;
list?: ListConfig;
toolbar?: boolean | string | ToolbarConfig;
slashMenu?: SlashMenuConfig;

// Events
onReady?: () => void;
Expand Down
Loading