PageSourceSearch

https://tanenbaum.org/wp-content/plugins/conditional-checkout-fiel…/assets/js/fme-ccfw-inputmask.js?ver=2.1.0

js tanenbaum.org collected 2026-10-02 02:14:58 UTC 29,419 bytes, 1,126 lines download raw bytes

1/**
2 * Input masking for text & tel fields (A4).
3 * Static tokens: 9 = digit, a = letter, * = alphanumeric.
4 * Digits 0-8 in a static mask are flexible slot markers (not fixed characters).
5 * Other punctuation ( -, (, ), etc. ) are fixed literals.
6 * Fixed text groups: wrap literals in [square brackets], e.g. [+380] (99) 999 99 99.
7 * Dynamic patterns: a{1,3}-9{1,3}
8 */
9(function ($) {
10  'use strict';
11
12  function charMatchesType(ch, type) {
13    if (type === 'digit') {
14      return /\d/.test(ch);
15    }
16    if (type === 'alpha') {
17      return /[a-zA-Z]/.test(ch);
18    }
19    if (type === 'alnum') {
20      return /[a-zA-Z0-9]/.test(ch);
21    }
22    return false;
23  }
24
25  function parseStaticMaskChar(ch) {
26    if (ch === '9') {
27      return { kind: 'slot', type: 'digit' };
28    }
29    if (ch === 'a') {
30      return { kind: 'slot', type: 'alpha' };
31    }
32    if (ch === '*') {
33      return { kind: 'slot', type: 'alnum' };
34    }
35    if (/[0-8]/.test(ch)) {
36      return { kind: 'slot', type: 'alnum' };
37    }
38    if (/[A-Za-z]/.test(ch)) {
39      return { kind: 'slot', type: 'alnum' };
40    }
41    return { kind: 'literal', char: ch };
42  }
43
44  function staticMaskHasValidBrackets(mask) {
45    var depth = 0;
46
47    for (var i = 0; i < mask.length; i++) {
48      var ch = mask.charAt(i);
49
50      if (ch === '[') {
51        if (depth > 0) {
52          return false;
53        }
54        depth++;
55      } else if (ch === ']') {
56        if (depth === 0) {
57          return false;
58        }
59        depth--;
60      }
61    }
62
63    return depth === 0;
64  }
65
66  function parseStaticMask(mask) {
67    var slots = [];
68    var i = 0;
69
70    while (i < mask.length) {
71      if (mask.charAt(i) === '[') {
72        var close = mask.indexOf(']', i + 1);
73        if (close === -1) {
74          slots.push(parseStaticMaskChar(mask.charAt(i)));
75          i++;
76          continue;
77        }
78
79        for (var k = i + 1; k < close; k++) {
80          slots.push({ kind: 'literal', char: mask.charAt(k), fixedGroup: true });
81        }
82
83        i = close + 1;
84        continue;
85      }
86
87      slots.push(parseStaticMaskChar(mask.charAt(i)));
88      i++;
89    }
90
91    return slots;
92  }
93
94  function staticMaskHasSlots(mask) {
95    var slots = parseStaticMask(mask);
96    for (var i = 0; i < slots.length; i++) {
97      if (slots[i].kind === 'slot') {
98        return true;
99      }
100    }
101    return false;
102  }
103
104  function escapeRegexLiteral(char) {
105    return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106  }
107
108  function getStaticMaskSlotRegex(type) {
109    if (type === 'digit') {
110      return '\\d';
111    }
112    if (type === 'alpha') {
113      return '[a-zA-Z]';
114    }
115    return '[a-zA-Z0-9]';
116  }
117
118  function buildStaticInputMaskRegex(mask) {
119    var tokens = parseStaticMask(mask);
120    var body = '';
121
122    for (var i = 0; i < tokens.length; i++) {
123      var token = tokens[i];
124      if (token.kind === 'literal') {
125        body += escapeRegexLiteral(token.char);
126      } else {
127        body += getStaticMaskSlotRegex(token.type);
128      }
129    }
130
131    if (!body) {
132      return null;
133    }
134
135    try {
136      return new RegExp('^' + body + '$');
137    } catch (e) {
138      return null;
139    }
140  }
141
142  function parseDynamicMaskSegments(mask) {
143    var segments = [];
144    var i = 0;
145
146    while (i < mask.length) {
147      var ch = mask.charAt(i);
148
149      if (ch === '[') {
150        var close = mask.indexOf(']', i + 1);
151        if (close === -1) {
152          segments.push({ type: 'literal', text: '[' });
153          i++;
154          continue;
155        }
156        var bracketText = mask.substring(i + 1, close);
157        if (bracketText) {
158          segments.push({ type: 'literal', text: bracketText, fixedGroup: true });
159        }
160        i = close + 1;
161        continue;
162      }
163
164      if (/[a9*]/.test(ch)) {
165        var token = ch;
166        i++;
167        var minCount = 1;
168        var maxCount = 1;
169
170        if (i < mask.length && mask.charAt(i) === '{') {
171          var rest = mask.substring(i);
172          var qm = rest.match(/^\{(\d+)(?:,(\d+))?\}/);
173          if (qm) {
174            minCount = parseInt(qm[1], 10);
175            maxCount = qm[2] !== undefined ? parseInt(qm[2], 10) : minCount;
176            i += qm[0].length;
177          }
178        }
179
180        segments.push({
181          type: 'slot',
182          token: token,
183          min: minCount,
184          max: maxCount
185        });
186        continue;
187      }
188
189      var lit = '';
190      while (i < mask.length) {
191        var next = mask.charAt(i);
192        if (next === '[' || /[a9*]/.test(next)) {
193          break;
194        }
195        lit += next;
196        i++;
197      }
198      if (lit) {
199        segments.push({ type: 'literal', text: lit });
200      }
201    }
202
203    return segments;
204  }
205
206  function buildDynamicInputMaskRegex(mask) {
207    var segments = parseDynamicMaskSegments(mask);
208    if (!segments.length) {
209      return null;
210    }
211
212    var body = '';
213
214    for (var i = 0; i < segments.length; i++) {
215      var segment = segments[i];
216
217      if (segment.type === 'literal') {
218        body += escapeRegexLiteral(segment.text);
219        continue;
220      }
221
222      var slot = getStaticMaskSlotRegex(
223        segment.token === '9' ? 'digit' : (segment.token === 'a' ? 'alpha' : 'alnum')
224      );
225
226      if (segment.min === segment.max) {
227        body += slot + '{' + segment.min + '}';
228      } else {
229        body += slot + '{' + segment.min + ',' + segment.max + '}';
230      }
231    }
232
233    if (!body) {
234      return null;
235    }
236
237    try {
238      return new RegExp('^' + body + '$');
239    } catch (e) {
240      return null;
241    }
242  }
243
244  window.fmeCcfwIsValidInputMask = function (mask, maskType) {
245    var pattern = String(mask || '').trim();
246    if (!pattern) {
247      return true;
248    }
249
250    if (!staticMaskHasValidBrackets(pattern)) {
251      return false;
252    }
253
254    if (maskType === 'dynamic') {
255      if (!/[a9*]\{\d+(?:,\d+)?\}/.test(pattern)) {
256        return false;
257      }
258
259      var cap = 50;
260      var segments = parseDynamicMaskSegments(pattern);
261      var hasSlot = false;
262
263      for (var s = 0; s < segments.length; s++) {
264        if (segments[s].type !== 'slot') {
265          continue;
266        }
267        hasSlot = true;
268        var min = parseInt(segments[s].min, 10);
269        var max = parseInt(segments[s].max, 10);
270        if (isNaN(min) || isNaN(max) || min < 0 || max < 1 || min > max || max > cap) {
271          return false;
272        }
273      }
274
275      return hasSlot;
276    }
277
278    if (/[{}]/.test(pattern)) {
279      return false;
280    }
281
282    return staticMaskHasSlots(pattern);
283  };
284
285  window.fmeCcfwIsInputMaskValueValid = function (value, config) {
286    if (!config || !config.mask) {
287      return true;
288    }
289
290    var trimmed = String(value || '').trim();
291    if (!trimmed) {
292      return true;
293    }
294
295    var mask = String(config.mask).trim();
296    var maskType = config.maskType || config.mask_type || 'static';
297
298    if (!window.fmeCcfwIsValidInputMask(mask, maskType)) {
299      return true;
300    }
301
302    var regex = maskType === 'dynamic'
303      ? buildDynamicInputMaskRegex(mask)
304      : buildStaticInputMaskRegex(mask);
305
306    if (!regex) {
307      return true;
308    }
309
310    return regex.test(trimmed);
311  };
312
313  function parseDynamicMask(mask) {
314    var slots = [];
315    var segments = parseDynamicMaskSegments(mask);
316
317    for (var s = 0; s < segments.length; s++) {
318      var segment = segments[s];
319      if (segment.type === 'slot') {
320        var slotType = segment.token === '9' ? 'digit' : (segment.token === 'a' ? 'alpha' : 'alnum');
321        for (var i = 0; i < segment.max; i++) {
322          slots.push({
323            kind: 'slot',
324            type: slotType,
325            optional: i >= segment.min
326          });
327        }
328      } else if (segment.type === 'literal') {
329        for (var j = 0; j < segment.text.length; j++) {
330          var litSlot = { kind: 'literal', char: segment.text.charAt(j) };
331          if (segment.fixedGroup) {
332            litSlot.fixedGroup = true;
333          }
334          slots.push(litSlot);
335        }
336      }
337    }
338
339    return slots;
340  }
341
342  function extractInputChars(value) {
343    var chars = [];
344    for (var i = 0; i < value.length; i++) {
345      if (/[a-zA-Z0-9]/.test(value.charAt(i))) {
346        chars.push(value.charAt(i));
347      }
348    }
349    return chars;
350  }
351
352  function getPrefixLiteralString(slots) {
353    var prefixLen = prefixLiteralCount(slots);
354    var prefix = '';
355
356    for (var i = 0; i < prefixLen; i++) {
357      prefix += slots[i].char;
358    }
359
360    return prefix;
361  }
362
363  /**
364   * Leading [bracket] fixed-group content only (e.g. "+92"), not following
365   * separator literals like " (" after `[+92] (999)...`.
366   */
367  function getBracketedFixedPrefixCore(slots) {
368    var core = '';
369
370    if (!slots || !slots.length) {
371      return core;
372    }
373
374    for (var i = 0; i < slots.length; i++) {
375      if (slots[i].kind !== 'literal' || !slots[i].fixedGroup) {
376        break;
377      }
378      core += slots[i].char;
379    }
380
381    return core;
382  }
383
384  function skipLeadingSeparatorChars(str) {
385    var i = 0;
386    while (i < str.length && !/[a-zA-Z0-9]/.test(str.charAt(i))) {
387      i++;
388    }
389    return str.substring(i);
390  }
391
392  function getPrefixAlnumCore(core) {
393    var alnum = '';
394    for (var i = 0; i < core.length; i++) {
395      if (/[a-zA-Z0-9]/.test(core.charAt(i))) {
396        alnum += core.charAt(i);
397      }
398    }
399    return alnum;
400  }
401
402  /**
403   * If value begins with the bracket-prefix core (ignoring [], spaces, and
404   * separators; non-alnum core chars like "+" are optional in the paste),
405   * return the remainder after that prefix. Otherwise return null.
406   */
407  function tryStripLeadingBracketPrefix(value, core) {
408    var coreAlnum = getPrefixAlnumCore(core);
409    if (!coreAlnum) {
410      return null;
411    }
412
413    var ci = 0;
414    var vi = 0;
415    while (vi < value.length && ci < coreAlnum.length) {
416      var ch = value.charAt(vi);
417      if (ch === '[' || ch === ']' || !/[a-zA-Z0-9]/.test(ch)) {
418        vi++;
419        continue;
420      }
421      if (ch.toLowerCase() === coreAlnum.charAt(ci).toLowerCase()) {
422        ci++;
423        vi++;
424        continue;
425      }
426      return null;
427    }
428
429    if (ci < coreAlnum.length) {
430      return null;
431    }
432
433    return value.substring(vi);
434  }
435
436  /**
437   * Strip a leading bracketed fixed prefix flexibly so paste like "+92 300...",
438   * "[+92] 300...", or "92 300..." does not feed prefix digits into slots.
439   */
440  function stripFixedPrefixFromValue(str, slots) {
441    var value = String(str || '');
442
443    if (!slots || !slots.length) {
444      return value;
445    }
446
447    var core = getBracketedFixedPrefixCore(slots);
448    if (core) {
449      var afterCore = tryStripLeadingBracketPrefix(value, core);
450      if (afterCore !== null) {
451        return skipLeadingSeparatorChars(afterCore);
452      }
453    }
454
455    var prefix = getPrefixLiteralString(slots);
456    if (prefix && value.indexOf(prefix) === 0) {
457      return value.substring(prefix.length);
458    }
459
460    return value;
461  }
462
463  function extractUserInputChars(value, slots) {
464    return extractInputChars(stripFixedPrefixFromValue(String(value || ''), slots));
465  }
466
467  function prefixLiteralCount(slots) {
468    var count = 0;
469    for (var i = 0; i < slots.length; i++) {
470      if (slots[i].kind === 'literal') {
471        count++;
472      } else {
473        break;
474      }
475    }
476    return count;
477  }
478
479  function slotsInSegmentBefore(slots, index) {
480    var count = 0;
481    for (var i = index - 1; i >= 0; i--) {
482      if (slots[i].kind === 'literal') {
483        break;
484      }
485      if (slots[i].kind === 'slot' && !slots[i].optional) {
486        count++;
487      }
488    }
489    return count;
490  }
491
492  function applyMaskSlotsWithMap(inputChars, slots) {
493    var result = '';
494    var map = [];
495    var vi = 0;
496    var segmentSlotCount = 0;
497    var prefixLen = prefixLiteralCount(slots);
498    var prefixAdded = false;
499
500    for (var mi = 0; mi < slots.length; mi++) {
501      var token = slots[mi];
502
503      if (token.kind === 'literal') {
504        if (mi < prefixLen) {
505          continue;
506        }
507
508        var needed = slotsInSegmentBefore(slots, mi);
509        if (needed > 0 && segmentSlotCount >= needed) {
510          result += token.char;
511          map.push({ type: 'literal' });
512          segmentSlotCount = 0;
513        }
514        continue;
515      }
516
517      if (vi >= inputChars.length) {
518        if (!token.optional) {
519          break;
520        }
521        continue;
522      }
523
524      while (vi < inputChars.length && !charMatchesType(inputChars[vi], token.type)) {
525        vi++;
526      }
527
528      if (vi >= inputChars.length) {
529        if (!token.optional) {
530          break;
531        }
532        continue;
533      }
534
535      if (prefixLen > 0 && !prefixAdded) {
536        for (var pi = 0; pi < prefixLen; pi++) {
537          result += slots[pi].char;
538          map.push({ type: 'literal' });
539        }
540        prefixAdded = true;
541      }
542
543      var inputIndex = vi;
544      result += inputChars[vi++];
545      map.push({ type: 'slot', inputIndex: inputIndex });
546      segmentSlotCount++;
547
548      var lookahead = mi + 1;
549      while (lookahead < slots.length && slots[lookahead].kind === 'literal') {
550        var neededLit = slotsInSegmentBefore(slots, lookahead);
551        if (segmentSlotCount >= neededLit) {
552          result += slots[lookahead].char;
553          map.push({ type: 'literal' });
554          segmentSlotCount = 0;
555          lookahead++;
556          mi = lookahead - 1;
557        } else {
558          break;
559        }
560      }
561    }
562
563    return { masked: result, map: map };
564  }
565
566  function applyMaskSlots(inputChars, slots) {
567    return applyMaskSlotsWithMap(inputChars, slots).masked;
568  }
569
570  window.fmeCcfwApplyInputMask = function (value, config) {
571    if (!config || !config.mask) {
572      return value || '';
573    }
574
575    var mask = String(config.mask).trim();
576    var maskType = config.maskType || config.mask_type || 'static';
577
578    if (!window.fmeCcfwIsValidInputMask(mask, maskType)) {
579      return value || '';
580    }
581
582    var slots = maskType === 'dynamic' ? parseDynamicMask(mask) : parseStaticMask(mask);
583    if (!slots.length) {
584      return value || '';
585    }
586
587    var inputChars = extractUserInputChars(String(value || ''), slots);
588    return applyMaskSlots(inputChars, slots);
589  };
590
591  function resolveMaskSlots(config) {
592    if (!config || !config.mask) {
593      return [];
594    }
595
596    var mask = String(config.mask).trim();
597    var maskType = config.maskType || config.mask_type || 'static';
598
599    if (!window.fmeCcfwIsValidInputMask(mask, maskType)) {
600      return [];
601    }
602
603    return maskType === 'dynamic' ? parseDynamicMask(mask) : parseStaticMask(mask);
604  }
605
606  function getStoredRawChars($input, fallbackValue, config) {
607    var stored = $input.data('fmeCcfwInputMaskRaw');
608    if (stored && Object.prototype.toString.call(stored) === '[object Array]') {
609      return stored.slice();
610    }
611
612    if (config) {
613      var slots = resolveMaskSlots(config);
614      return extractUserInputChars(String(fallbackValue || ''), slots);
615    }
616
617    return extractInputChars(String(fallbackValue || ''));
618  }
619
620  function cursorAfterRemovingInput(map, removedIndex, maskedLength) {
621    for (var i = 0; i < map.length; i++) {
622      if (map[i].type === 'slot' && map[i].inputIndex === removedIndex) {
623        return i;
624      }
625    }
626
627    return maskedLength;
628  }
629
630  function findInputIndexBefore(map, displayIndex) {
631    for (var i = displayIndex; i >= 0; i--) {
632      if (map[i] && map[i].type === 'slot') {
633        return map[i].inputIndex;
634      }
635    }
636
637    return -1;
638  }
639
640  function findInputIndexAtOrAfter(map, displayIndex) {
641    for (var i = displayIndex; i < map.length; i++) {
642      if (map[i].type === 'slot') {
643        return map[i].inputIndex;
644      }
645    }
646
647    return -1;
648  }
649
650  function syncMaskState($input, rawChars, config, cursorPos) {
651    var slots = resolveMaskSlots(config);
652    if (!slots.length) {
653      return null;
654    }
655
656    var applied = applyMaskSlotsWithMap(rawChars, slots);
657    $input.val(applied.masked);
658    $input.data('fmeCcfwInputMaskRaw', rawChars.slice());
659    $input.data('fmeCcfwInputMaskValue', applied.masked);
660
661    if (cursorPos !== undefined && $input[0] && typeof $input[0].setSelectionRange === 'function') {
662      try {
663        $input[0].setSelectionRange(cursorPos, cursorPos);
664      } catch (e) {
665        // Ignore inputs that do not support selection.
666      }
667    }
668
669    return applied;
670  }
671
672  function applyRawMask(rawChars, config) {
673    var slots = resolveMaskSlots(config);
674    if (!slots.length) {
675      return { masked: rawChars.join(''), raw: rawChars.slice(), map: [] };
676    }
677
678    var applied = applyMaskSlotsWithMap(rawChars, slots);
679    return { masked: applied.masked, raw: rawChars.slice(), map: applied.map };
680  }
681
682  function processMaskInput(prevMasked, rawChars, current, config) {
683    var slots = resolveMaskSlots(config);
684    var prevRaw = rawChars && rawChars.length ? rawChars.slice() : extractUserInputChars(prevMasked, slots);
685    var currRaw = extractUserInputChars(current, slots);
686    var raw = currRaw;
687
688    if (current.length < prevMasked.length && currRaw.length >= prevRaw.length) {
689      raw = prevRaw.slice(0, -1);
690    }
691
692    return applyRawMask(raw, config);
693  }
694
695  function processMaskDeletion(rawChars, config, key, selectionStart, selectionEnd) {
696    var raw = rawChars.slice();
697    var slots = resolveMaskSlots(config);
698    if (!slots.length) {
699      return { handled: false };
700    }
701
702    var current = applyMaskSlotsWithMap(raw, slots);
703    var map = current.map;
704    var masked = current.masked;
705    var removeIndex = -1;
706    var cursorPos;
707
708    if (selectionStart !== selectionEnd) {
709      var removeIndices = {};
710      for (var i = selectionStart; i < selectionEnd && i < map.length; i++) {
711        if (map[i].type === 'slot') {
712          removeIndices[map[i].inputIndex] = true;
713        }
714      }
715
716      if (!Object.keys(removeIndices).length) {
717        return { handled: false };
718      }
719
720      raw = raw.filter(function (_, idx) {
721        return !removeIndices[idx];
722      });
723
724      var selectionResult = applyRawMask(raw, config);
725      selectionResult.handled = true;
726      selectionResult.cursorPos = selectionStart;
727      return selectionResult;
728    }
729
730    if (key === 'Backspace') {
731      if (selectionStart === 0) {
732        return { handled: true };
733      }
734
735      var backspaceTarget = map[selectionStart - 1];
736      if (!backspaceTarget) {
737        return { handled: true };
738      }
739
740      if (backspaceTarget.type === 'slot') {
741        removeIndex = backspaceTarget.inputIndex;
742      } else {
743        removeIndex = findInputIndexBefore(map, selectionStart - 1);
744      }
745    } else if (key === 'Delete') {
746      if (selectionStart >= masked.length) {
747        return { handled: true };
748      }
749
750      var deleteTarget = map[selectionStart];
751      if (!deleteTarget) {
752        return { handled: true };
753      }
754
755      if (deleteTarget.type === 'slot') {
756        removeIndex = deleteTarget.inputIndex;
757      } else {
758        removeIndex = findInputIndexAtOrAfter(map, selectionStart + 1);
759      }
760    }
761
762    if (removeIndex < 0) {
763      return { handled: true };
764    }
765
766    var oldMap = map;
767    var oldMaskedLength = masked.length;
768    raw.splice(removeIndex, 1);
769    var next = applyRawMask(raw, config);
770    next.handled = true;
771    next.cursorPos = Math.min(
772      cursorAfterRemovingInput(oldMap, removeIndex, oldMaskedLength),
773      next.masked.length
774    );
775    return next;
776  }
777
778  function countMaskSlots(slots) {
779    var count = 0;
780    for (var i = 0; i < slots.length; i++) {
781      if (slots[i].kind === 'slot') {
782        count++;
783      }
784    }
785    return count;
786  }
787
788  function applyPasteValue(pastedText, config) {
789    var slots = resolveMaskSlots(config);
790    if (!slots.length) {
791      return { masked: String(pastedText || ''), raw: extractInputChars(String(pastedText || '')), map: [] };
792    }
793
794    var raw = extractUserInputChars(String(pastedText || ''), slots);
795    var maxSlots = countMaskSlots(slots);
796    if (raw.length > maxSlots) {
797      raw = raw.slice(0, maxSlots);
798    }
799
800    return applyRawMask(raw, config);
801  }
802
803  window.fmeCcfwCreateInputMaskController = function (config) {
804    var state = {
805      raw: [],
806      masked: ''
807    };
808
809    function setState(result) {
810      state.raw = result.raw.slice();
811      state.masked = result.masked;
812      return result;
813    }
814
815    return {
816      init: function (value) {
817        var slots = resolveMaskSlots(config);
818        return setState(applyRawMask(extractUserInputChars(String(value || ''), slots), config));
819      },
820      applyInput: function (prevMasked, current) {
821        return setState(processMaskInput(prevMasked, state.raw, current, config));
822      },
823      applyPaste: function (pastedText) {
824        return setState(applyPasteValue(pastedText, config));
825      },
826      handleKeydown: function (key, selectionStart, selectionEnd) {
827        var result = processMaskDeletion(state.raw, config, key, selectionStart, selectionEnd);
828        if (result.raw) {
829          setState(result);
830        }
831        return result;
832      },
833      getMasked: function () {
834        return state.masked;
835      },
836      getRaw: function () {
837        return state.raw.slice();
838      }
839    };
840  };
841
842  function handleMaskDeletion($input, config, key, selectionStart, selectionEnd) {
843    var raw = getStoredRawChars($input, $input.val(), config);
844    var result = processMaskDeletion(raw, config, key, selectionStart, selectionEnd);
845
846    if (!result.handled) {
847      return false;
848    }
849
850    if (result.raw) {
851      syncMaskState($input, result.raw, config, result.cursorPos);
852      if ('' === $input.val()) {
853        $input.trigger('change');
854      }
855    }
856
857    return true;
858  }
859
860  function handleMaskInput($input, config) {
861    var prevMasked = $input.data('fmeCcfwInputMaskValue') || '';
862    var prevRaw = getStoredRawChars($input, prevMasked, config);
863    var result = processMaskInput(prevMasked, prevRaw, $input.val(), config);
864    syncMaskState($input, result.raw, config);
865  }
866
867  function readConfig($input) {
868    var raw = $input.attr('data-fme-input-mask-config');
869    if (raw) {
870      try {
871        var parsed = JSON.parse(raw);
872        if (parsed && parsed.mask) {
873          return parsed;
874        }
875      } catch (e) {
876        // Fall through to data-fme-input-mask attrs (legacy / malformed JSON).
877      }
878    }
879
880    var mask = $input.attr('data-fme-input-mask');
881    if (!mask) {
882      return null;
883    }
884
885    return {
886      mask: mask,
887      maskType: $input.attr('data-fme-input-mask-type') || 'static'
888    };
889  }
890
891  /**
892   * Build a validation message for a masked field.
893   */
894  function getMaskValidationMessage(config, $input) {
895    var hint = (config && (config.formatHint || config.format_hint)) || '';
896    if (!hint && $input && $input.length) {
897      hint = String($input.attr('data-fme-input-mask-hint') || '').trim();
898    }
899    var base = 'does not match the required format.';
900    return hint ? (base + ' ' + hint) : base;
901  }
902
903  function clearMaskFieldError($input) {
904    var $row = $input.closest('.form-row');
905    $row.removeClass('woocommerce-invalid woocommerce-invalid-format');
906    $row.find('span.fme-ccfw-input-mask-error').remove();
907    // Also clear sibling spans (block checkout has no .form-row; never leave
908    // free-floating error text next to Woo address inputs in the flex grid).
909    $input.siblings('span.fme-ccfw-input-mask-error').remove();
910    $input.next('span.fme-ccfw-input-mask-error').remove();
911  }
912
913  /**
914   * Classic checkout only: inject error inside .form-row.
915   * Woo Blocks address fields are flex-grid ControlledTextInputs — inserting a
916   * sibling span beside the input breaks First name / Last name layout.
917   * Block default-field masks use wc/store/validation instead.
918   */
919  function showMaskFieldError($input, message) {
920    clearMaskFieldError($input);
921
922    var $row = $input.closest('.form-row');
923    if (!$row.length) {
924      // Block / non-classic markup: do not inject DOM next to the input.
925      return;
926    }
927
928    $row.removeClass('woocommerce-validated').addClass('woocommerce-invalid woocommerce-invalid-format');
929    $input.after(
930      $('<span/>', {
931        class: 'fme-ccfw-input-mask-error error',
932        role: 'alert',
933        text: message || getMaskValidationMessage(readConfig($input), $input)
934      })
935    );
936  }
937
938  function isMaskedFieldHidden($input) {
939    var name = $input.attr('name') || $input.attr('id') || '';
940    if (!name) {
941      return false;
942    }
943    var $status = $('input[name="fme_field_status_' + name + '"]');
944    if ($status.length && $status.val() === 'hide') {
945      return true;
946    }
947    var $row = $input.closest('.form-row');
948    if ($row.length && ($row.is(':hidden') || $row.css('display') === 'none')) {
949      return true;
950    }
951    return $input.is(':hidden') && !$input.is('[type="hidden"]');
952  }
953
954  /**
955   * Validate a single masked input. Does NOT remask — normalize OFF raw paste stays until typing.
956   * @return {boolean}
957   */
958  window.fmeCcfwValidateMaskedInput = function ($input) {
959    if (!$input || !$input.length) {
960      return true;
961    }
962
963    var config = readConfig($input);
964    if (!config || !config.mask) {
965      return true;
966    }
967
968    if (isMaskedFieldHidden($input)) {
969      clearMaskFieldError($input);
970      return true;
971    }
972
973    var value = String($input.val() || '');
974    if (!String(value).trim()) {
975      clearMaskFieldError($input);
976      return true;
977    }
978
979    var valid = window.fmeCcfwIsInputMaskValueValid(value, config);
980    if (valid) {
981      clearMaskFieldError($input);
982      return true;
983    }
984
985    showMaskFieldError($input, getMaskValidationMessage(config, $input));
986    return false;
987  };
988
989  window.fmeCcfwValidateAllInputMasks = function (context) {
990    var allValid = true;
991    var $firstInvalid = null;
992
993    $(context || document).find('[data-fme-input-mask], [data-fme-input-mask-config]').each(function () {
994      var $input = $(this);
995      if (!window.fmeCcfwValidateMaskedInput($input)) {
996        allValid = false;
997        if (!$firstInvalid) {
998          $firstInvalid = $input;
999        }
1000      }
1001    });
1002
1003    if ($firstInvalid && $firstInvalid.length) {
1004      try {
1005        $firstInvalid.trigger('focus');
1006        $('html, body').animate({ scrollTop: $firstInvalid.offset().top - 100 }, 200);
1007      } catch (e) {
1008        // Ignore focus/scroll failures.
1009      }
1010    }
1011
1012    return allValid;
1013  };
1014
1015  function bindInputMask($input, config) {
1016    if (!$input || !$input.length || !config || !config.mask) {
1017      return false;
1018    }
1019
1020    if (!window.fmeCcfwIsValidInputMask(config.mask, config.maskType || config.mask_type || 'static')) {
1021      return false;
1022    }
1023
1024    if ($input.data('fmeCcfwInputMaskBound')) {
1025      return true;
1026    }
1027
1028    $input.data('fmeCcfwInputMaskBound', true);
1029    $input.attr('data-fme-input-mask-config', JSON.stringify(config));
1030    syncMaskState($input, extractUserInputChars($input.val() || '', resolveMaskSlots(config)), config);
1031
1032    $input.on('keydown.fmeCcfwInputMask', function (e) {
1033      if (e.key !== 'Backspace' && e.key !== 'Delete') {
1034        return;
1035      }
1036
1037      var el = $input[0];
1038      if (!el || typeof el.selectionStart !== 'number') {
1039        return;
1040      }
1041
1042      if (handleMaskDeletion($input, config, e.key, el.selectionStart, el.selectionEnd)) {
1043        e.preventDefault();
1044      }
1045    });
1046
1047    $input.on('input.fmeCcfwInputMask', function () {
1048      handleMaskInput($input, config);
1049      clearMaskFieldError($input);
1050    });
1051
1052    $input.on('paste.fmeCcfwInputMask', function (e) {
1053      var clipboard = (e.originalEvent && e.originalEvent.clipboardData) || window.clipboardData;
1054      if (!clipboard || typeof clipboard.getData !== 'function') {
1055        return;
1056      }
1057
1058      var pasted = clipboard.getData('text');
1059      if (pasted === null || pasted === undefined) {
1060        return;
1061      }
1062
1063      e.preventDefault();
1064      var result = applyPasteValue(pasted, config);
1065      syncMaskState($input, result.raw, config, result.masked.length);
1066      clearMaskFieldError($input);
1067      $input.trigger('change');
1068    });
1069
1070    // Blur: always validate against mask.
1071    $input.on('blur.fmeCcfwInputMask', function () {
1072      window.fmeCcfwValidateMaskedInput($input);
1073    });
1074
1075    return true;
1076  }
1077
1078  window.fmeCcfwInitInputmask = function ($input, config) {
1079    if (!$input || !$input.length) {
1080      return false;
1081    }
1082
1083    var resolvedConfig = config || readConfig($input);
1084    return bindInputMask($input, resolvedConfig);
1085  };
1086
1087  window.fmeCcfwInitInputmasksInContext = function (context) {
1088    var initialized = 0;
1089    $(context || document).find('[data-fme-input-mask]').each(function () {
1090      if (window.fmeCcfwInitInputmask($(this))) {
1091        initialized++;
1092      }
1093    });
1094    return initialized;
1095  };
1096
1097  function scheduleInputmaskInit(context, attempt) {
1098    attempt = attempt || 0;
1099    window.fmeCcfwInitInputmasksInContext(context || document);
1100    if (attempt < 5) {
1101      setTimeout(function () {
1102        scheduleInputmaskInit(context, attempt + 1);
1103      }, 200);
1104    }
1105  }
1106
1107  window.fmeCcfwScheduleInputmaskInit = scheduleInputmaskInit;
1108
1109  $(document).ready(function () {
1110    scheduleInputmaskInit(document);
1111  });
1112
1113  $(document.body).on('updated_checkout', function () {
1114    setTimeout(function () {
1115      scheduleInputmaskInit(document);
1116    }, 150);
1117  });
1118
1119  // Classic checkout submit: mask validity always required.
1120  $(document.body).on('checkout_place_order', 'form.checkout', function () {
1121    if (typeof window.fmeCcfwValidateAllInputMasks !== 'function') {
1122      return true;
1123    }
1124    return window.fmeCcfwValidateAllInputMasks(document);
1125  });
1126})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.