@@ -376,12 +376,13 @@ function extractValueFromObject(item: any): any {
376376}
377377
378378/**
379- * Helper function to extract value from object format for searchMethod-based controls
379+ * Helper function to normalize control values - extracts values from object formats
380+ * and parses JSON strings for jsonTextArea controls
380381 * @param control - The control object
381- * @param value - The value to extract from
382- * @returns The extracted value
382+ * @param value - The value to normalize
383+ * @returns The normalized value
383384 */
384- function extractSearchMethodValue ( control : any , value : any ) {
385+ function normalizeControlValue ( control : any , value : any ) {
385386 // @ts -ignore
386387 if ( control . searchMethod && control . type === 'select' ) {
387388 return extractValueFromObject ( value )
@@ -392,6 +393,11 @@ function extractSearchMethodValue(control: any, value: any) {
392393 return value . map ( extractValueFromObject )
393394 }
394395 }
396+ // Parse JSON string to object for jsonTextArea
397+ else if ( control . type === 'jsonTextArea' ) {
398+ const { parsed } = parseJSON ( value )
399+ return parsed
400+ }
395401 return value
396402}
397403
@@ -425,6 +431,53 @@ const parseListOfTexts = (value:any)=>{
425431 return value ;
426432}
427433
434+ function parseJSON ( value : any ) : { parsed : any ; error : string | null } {
435+ if ( typeof value !== 'string' ) {
436+ return { parsed : value , error : null } ;
437+ }
438+
439+ const trimmedValue = value . trim ( ) ;
440+ if ( trimmedValue === '' ) {
441+ return { parsed : null , error : null } ;
442+ }
443+
444+ try {
445+ const parsed = JSON . parse ( trimmedValue ) ;
446+ return { parsed, error : null } ;
447+ } catch ( e : any ) {
448+ // Provide helpful error messages based on common JSON mistakes
449+ const errorMessage = e . message || 'Invalid JSON' ;
450+
451+ // Extract position info if available
452+ const positionMatch = errorMessage . match ( / p o s i t i o n \s + ( \d + ) / i) ;
453+ const position = positionMatch ? parseInt ( positionMatch [ 1 ] , 10 ) : null ;
454+
455+ let helpfulMessage = 'Invalid JSON: ' ;
456+
457+ // Check for common mistakes
458+ if ( trimmedValue . includes ( "'" ) && ! trimmedValue . includes ( '"' ) ) {
459+ helpfulMessage += "Use double quotes (\") instead of single quotes (') for strings." ;
460+ } else if ( / , \s * [ } \] ] / . test ( trimmedValue ) ) {
461+ helpfulMessage += "Trailing comma found. Remove the comma before the closing bracket." ;
462+ } else if ( errorMessage . includes ( 'Unexpected token' ) ) {
463+ if ( position !== null ) {
464+ const contextStart = Math . max ( 0 , position - 10 ) ;
465+ const contextEnd = Math . min ( trimmedValue . length , position + 10 ) ;
466+ const context = trimmedValue . substring ( contextStart , contextEnd ) ;
467+ helpfulMessage += `Unexpected character near position ${ position } : "...${ context } ..."` ;
468+ } else {
469+ helpfulMessage += errorMessage ;
470+ }
471+ } else if ( errorMessage . includes ( 'Unexpected end' ) ) {
472+ helpfulMessage += "JSON is incomplete. Check for missing closing brackets or quotes." ;
473+ } else {
474+ helpfulMessage += errorMessage ;
475+ }
476+
477+ return { parsed : null , error : helpfulMessage } ;
478+ }
479+ }
480+
428481class Controls {
429482 private isSectionControl = false ;
430483
@@ -605,6 +658,10 @@ class Controls {
605658 return this . add < string > ( id , "textarea" , { defaultValue : "" , ...props } )
606659 }
607660
661+ jsonTextArea ( id : string , props : TextControlInput < string | object > = { } ) {
662+ return this . add < string | object > ( id , "jsonTextArea" , { defaultValue : "" , ...props } )
663+ }
664+
608665 link ( id : string , props : LinkControlInput < string > = { } ) {
609666 return this . add < string > ( id , "link" , { defaultValue : "" , ...props } )
610667 }
@@ -781,7 +838,7 @@ private parse(data: any) {
781838 const controlId = control . id
782839 let value = data . hasOwnProperty ( controlId ) ? data [ controlId ] : null
783840
784- value = extractSearchMethodValue ( control , value )
841+ value = normalizeControlValue ( control , value )
785842
786843 mergedData [ controlId ] = value
787844 } )
@@ -791,7 +848,7 @@ private parse(data: any) {
791848 const controlId = control . id
792849 let value = data . hasOwnProperty ( controlId ) ? data [ controlId ] : defaultData [ controlId ]
793850
794- value = extractSearchMethodValue ( control , value )
851+ value = normalizeControlValue ( control , value )
795852
796853 mergedData [ controlId ] = value
797854 } )
@@ -830,6 +887,12 @@ private parse(data: any) {
830887 value = value . trim ( )
831888 }
832889 }
890+ else if ( control . type === "jsonTextArea" ) {
891+ // Only trim if it's a string, objects are already parsed
892+ if ( typeof value === "string" && canTrim ( control ) ) {
893+ value = value . trim ( )
894+ }
895+ }
833896 else if ( control . type === "listOfTexts" ) {
834897 value = value . filter ( isNotEmpty )
835898 // @ts -ignore
@@ -930,6 +993,17 @@ private parse(data: any) {
930993 )
931994 }
932995
996+ // JSON validation for jsonTextArea (only validate if value is a string)
997+ if (
998+ ! errorMessages . length && type === "jsonTextArea" &&
999+ isNotEmpty ( value )
1000+ ) {
1001+ const { error } = parseJSON ( value )
1002+ if ( error ) {
1003+ errorMessages . push ( error )
1004+ }
1005+ }
1006+
9331007 if (
9341008 ! errorMessages . length && type === "listOfLinks"
9351009 ) {
@@ -1044,6 +1118,10 @@ private parse(data: any) {
10441118 if ( typeof value !== "string" )
10451119 return "This field must be of type string."
10461120 break
1121+ case "jsonTextArea" :
1122+
1123+ // jsonTextArea can be a string or object, or anything
1124+ break
10471125 case "number" :
10481126 if ( typeof value !== "number" && value !== null )
10491127 return "This field must be of type number or null."
0 commit comments