@@ -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 - z A - Z 0 - 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 ;
0 commit comments