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.