@@ -402,7 +402,7 @@ class FastForm extends HTMLElement {
402402 const state = this . states . init ( name , initialState )
403403 if ( typeof clear !== "function" ) {
404404 clear = ( state && typeof state . values === "function" )
405- ? ( ) => Array . from ( state . values ( ) ) . forEach ( val => val && typeof val . clear === "function" && val . clear ( ) )
405+ ? ( ) => Array . from ( state . values ( ) ) . forEach ( s => s && typeof s . clear === "function" && s . clear ( ) )
406406 : ( ) => void 0
407407 }
408408 this . registerCleanup ( ( ) => clear ( state ) )
@@ -1449,74 +1449,30 @@ const Feature_Watchers = (() => {
14491449 }
14501450} ) ( )
14511451
1452- function compileMatchers ( { source, strategy, processValue, errorContext } ) {
1453- if ( ! source || typeof source !== "object" ) {
1454- return [ ]
1455- }
1456- const escapeRegex = ( string ) => string . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" )
1457- return Object . entries ( source ) . map ( ( [ key , rawValue ] ) => {
1458- const payload = processValue ( rawValue , key )
1459- if ( payload == null ) return
1460- try {
1461- const exp = ( strategy === "regex" )
1462- ? key
1463- : ( strategy === "wildcard" )
1464- ? `^${ escapeRegex ( key ) . replace ( / \\ \* / g, ".*" ) } $`
1465- : `^${ escapeRegex ( key ) } $`
1466- const regex = new RegExp ( exp )
1467- return { key, regex, ...payload }
1468- } catch ( e ) {
1469- throw new TypeError ( `Invalid ${ errorContext } pattern for '${ strategy } ' mode: '${ key } '.` )
1470- }
1471- } ) . filter ( Boolean )
1472- }
1473-
14741452const Feature_Parsing = {
14751453 featureOptions : {
14761454 parsers : { } ,
1477- parserMatchStrategy : "exact" , // "exact", "wildcard", "regex"
14781455 } ,
14791456 configure : ( { hooks, initState, registerApi } ) => {
1480- const state = initState (
1481- { rawParsers : new Map ( ) , compiledParsers : [ ] } ,
1482- state => {
1483- state . rawParsers . clear ( )
1484- state . compiledParsers = [ ]
1485- } ,
1486- )
1457+ const parsers = initState ( new Map ( ) , s => s . clear ( ) )
14871458 registerApi ( "parsing" , {
1488- setParser : ( key , parserToAdd ) => {
1459+ set : ( key , parserToAdd ) => {
14891460 if ( key && typeof parserToAdd === "function" ) {
1490- state . rawParsers . set ( key , parserToAdd )
1461+ parsers . set ( key , parserToAdd )
14911462 } else {
14921463 console . warn ( `FastForm Warning: Parser for key '${ key } ' is not a function.` )
14931464 }
1494- }
1465+ } ,
1466+ get : ( key ) => parsers . get ( key ) ,
14951467 } )
14961468 hooks . on ( "onProcessValue" , ( value , changeContext ) => {
1497- const newChangeContext = { ...changeContext , value }
1498- const matchedParsers = state . compiledParsers . filter ( p => p . regex . test ( newChangeContext . key ) )
1499- if ( matchedParsers . length === 0 ) {
1500- return value
1501- }
1502- if ( matchedParsers . length > 1 ) {
1503- matchedParsers . sort ( ( a , b ) => b . key . length - a . key . length )
1504- }
1505- return matchedParsers [ 0 ] . parser ( value , newChangeContext )
1469+ const parser = parsers . get ( changeContext . key )
1470+ return parser ? parser ( value , changeContext ) : value
15061471 } )
15071472 } ,
1508- compile : ( { state, options, form } ) => {
1509- const { parsers, parserMatchStrategy } = options
1510-
1473+ compile : ( { options, form } ) => {
15111474 const api = form . getApi ( "parsing" )
1512- Object . entries ( parsers ) . forEach ( ( [ key , rule ] ) => api . setParser ( key , rule ) )
1513-
1514- state . compiledParsers = compileMatchers ( {
1515- source : Object . fromEntries ( state . rawParsers ) ,
1516- strategy : parserMatchStrategy ,
1517- errorContext : "parser" ,
1518- processValue : ( parser ) => ( { parser } )
1519- } )
1475+ Object . entries ( options . parsers ) . forEach ( ( [ key , rule ] ) => api . set ( key , rule ) )
15201476 } ,
15211477}
15221478
@@ -1580,13 +1536,7 @@ const Feature_Validation = {
15801536 } ) . filter ( Boolean )
15811537 } ,
15821538 configure : ( { initState, hooks, registerApi, form } ) => {
1583- const state = initState (
1584- { rawRules : new Map ( ) , compiledRules : new Map ( ) } ,
1585- state => {
1586- state . rawRules . clear ( )
1587- state . compiledRules . clear ( )
1588- }
1589- )
1539+ const state = initState ( { rawRules : new Map ( ) , compiledRules : new Map ( ) } )
15901540 registerApi ( "validation" , {
15911541 addRule : ( key , ruleConfig ) => {
15921542 if ( ! key || ! ruleConfig ) return
@@ -2222,7 +2172,7 @@ const Control_Color = {
22222172 const input = element . querySelector ( ".color-input" )
22232173 const display = element . querySelector ( ".color-display" )
22242174 if ( input && display ) {
2225- value = value || "#000000 "
2175+ value = value || "#FFFFFF "
22262176 updateInputState ( input , field , value )
22272177 display . textContent = value . toUpperCase ( )
22282178 }
@@ -2507,6 +2457,86 @@ const Control_Textarea = {
25072457 } ,
25082458}
25092459
2460+ const Control_CodeEditor = {
2461+ controlOptions : {
2462+ tabSize : 4 ,
2463+ lineNumbers : true ,
2464+ } ,
2465+ setup : ( { field } ) => defaultBlockLayout ( field ) ,
2466+ create : ( { field, controlOptions } ) => {
2467+ const { lineNumbers } = controlOptions
2468+ const { key, placeholder } = getCommonHTMLAttrs ( field )
2469+ const gutterClass = lineNumbers ? "code-gutter" : "code-gutter plugin-common-hidden"
2470+ const textarea = `<textarea class="code-textarea" ${ placeholder } spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>`
2471+ return `<div class="code-editor-wrap" ${ key } ><div class="${ gutterClass } "></div><div class="code-grow-wrap"><div class="code-ghost"></div>${ textarea } </div></div>`
2472+ } ,
2473+ update : ( { element, value, field, controlOptions } ) => {
2474+ const textarea = element . querySelector ( ".code-textarea" )
2475+ if ( ! textarea ) return
2476+ const ghost = element . querySelector ( ".code-ghost" )
2477+ const gutter = element . querySelector ( ".code-gutter" )
2478+ const val = value || ""
2479+ if ( textarea . value !== val ) textarea . value = val
2480+ if ( ghost ) ghost . textContent = val + "\n"
2481+ updateInputState ( textarea , field , val )
2482+ if ( controlOptions . lineNumbers && gutter ) {
2483+ Control_CodeEditor . _updateLineNumbers ( textarea , gutter )
2484+ }
2485+ } ,
2486+ bindEvents : ( { form } ) => {
2487+ const syncState = ( textarea ) => {
2488+ const wrap = textarea . closest ( ".code-editor-wrap" )
2489+ const ghost = wrap . querySelector ( ".code-ghost" )
2490+ const gutter = wrap . querySelector ( ".code-gutter" )
2491+ ghost . textContent = textarea . value + "\n"
2492+ if ( gutter && ! gutter . classList . contains ( "plugin-common-hidden" ) ) {
2493+ Control_CodeEditor . _updateLineNumbers ( textarea , gutter )
2494+ }
2495+ }
2496+
2497+ form . onEvent ( "input" , ".code-textarea" , function ( ) {
2498+ syncState ( this )
2499+ } ) . onEvent ( "change" , ".code-textarea" , function ( ) {
2500+ form . validateAndCommit ( this . closest ( ".code-editor-wrap" ) . dataset . key , this . value )
2501+ } ) . onEvent ( "scroll" , ".code-textarea" , function ( ) {
2502+ const gutter = this . closest ( ".code-editor-wrap" ) . querySelector ( ".code-gutter" )
2503+ if ( gutter ) gutter . scrollTop = this . scrollTop
2504+ } ) . onEvent ( "keydown" , ".code-textarea" , function ( ev ) {
2505+ const key = this . closest ( ".code-editor-wrap" ) . dataset . key
2506+ const { tabSize } = form . getControlOptionsFromKey ( key )
2507+ if ( ev . key === "Tab" ) {
2508+ ev . preventDefault ( )
2509+ const spaces = " " . repeat ( tabSize )
2510+ Control_CodeEditor . _insertText ( this , spaces )
2511+ syncState ( this )
2512+ form . validateAndCommit ( key , this . value )
2513+ } else if ( ev . key === "Enter" ) {
2514+ ev . preventDefault ( )
2515+ const cursor = this . selectionStart
2516+ const currentLineStart = this . value . lastIndexOf ( "\n" , cursor - 1 ) + 1
2517+ const currentLine = this . value . substring ( currentLineStart , cursor )
2518+ const match = currentLine . match ( / ^ \s + / )
2519+ const indentation = match ? match [ 0 ] : ""
2520+ Control_CodeEditor . _insertText ( this , "\n" + indentation )
2521+ syncState ( this )
2522+ this . blur ( )
2523+ this . focus ( )
2524+ form . validateAndCommit ( key , this . value )
2525+ }
2526+ } , true )
2527+ } ,
2528+ _updateLineNumbers : ( textarea , gutter ) => {
2529+ const lineCount = textarea . value . split ( "\n" ) . length
2530+ if ( gutter . childElementCount === lineCount ) return
2531+ gutter . innerHTML = Array . from ( { length : lineCount } , ( _ , i ) => `<div>${ i + 1 } </div>` ) . join ( "" )
2532+ } ,
2533+ _insertText : ( textarea , text ) => {
2534+ const start = textarea . selectionStart
2535+ const end = textarea . selectionEnd
2536+ textarea . setRangeText ( text , start , end , "end" )
2537+ } ,
2538+ }
2539+
25102540const Control_Object = {
25112541 controlOptions : {
25122542 format : "JSON" ,
@@ -3114,11 +3144,7 @@ const Control_Transfer = {
31143144 item . animate ( [
31153145 { transform : `translate(${ dx } px, ${ dy } px)` } ,
31163146 { transform : "translate(0, 0)" }
3117- ] , {
3118- duration : 200 ,
3119- easing : "cubic-bezier(0.2, 0, 0, 1)" ,
3120- fill : "both"
3121- } )
3147+ ] , { duration : 200 , easing : "cubic-bezier(0.2, 0, 0, 1)" , fill : "both" } )
31223148 }
31233149 } )
31243150 } ,
@@ -3632,6 +3658,7 @@ FastForm.registerControl("custom", Control_Custom)
36323658FastForm . registerControl ( "hint" , Control_Hint )
36333659FastForm . registerControl ( "hotkey" , Control_Hotkey )
36343660FastForm . registerControl ( "textarea" , Control_Textarea )
3661+ FastForm . registerControl ( "code" , Control_CodeEditor )
36353662FastForm . registerControl ( "object" , Control_Object )
36363663FastForm . registerControl ( "array" , Control_Array )
36373664FastForm . registerControl ( "select" , Control_Select )
0 commit comments