1/** 2 * Front-end number-input stepper: wraps a number input in a Bootstrap 3 * input-group with "-" (before) and "+" (after) buttons. 4 * 5 * Progressive enhancement â the native input keeps working if this 6 * script fails, and it covers every source (PHP templates, CF7 7 * form-tags, and JS-injected DOM like the reprice-refreshed variation 8 * table / cart) via a MutationObserver, with no per-form wiring. 9 * 10 * Targets: 11 * - input[type=number].gc-stepper (marked template / cart inputs) 12 * - input[type=number].wpcf7-number (all CF7 pax/number fields) 13 * Opt out per input with class `gc-no-stepper`. 14 * 15 * CRITICAL: the +/- buttons dispatch native `input` + `change` events, 16 * so live-pricing subscribers (cart reprice, 2-step recompute, etc.) 17 * fire exactly as if the operator typed â never set `.value` silently. 18 */ 19(function () { 20 'use strict'; 21 22 var SELECTOR = 'input[type="number"].gc-stepper, input[type="number"].wpcf7-number'; 23 var READY_ATTR = 'data-gc-stepper-ready'; 24 var i18n = window.gcStepper || {}; 25 26 function toNum(v, fallback) { 27 var n = parseFloat(v); 28 return isNaN(n) ? fallback : n; 29 } 30 31 // The value an EMPTY field should start on: its explicit default (the HTML 32 // `value` attribute) if any, else its `min` WHEN that min is a positive, 33 // required floor (e.g. min:1). Returns NaN for optional fields (no default, 34 // min 0 / none) â those stay blank so we never invent a "0"/"1" the form 35 // treats as unset (e.g. the multi-option pax fields, "at least one ⥠1"). 36 function fieldStart(input) { 37 var def = toNum(input.defaultValue, NaN); 38 if (!isNaN(def)) return def; 39 var min = toNum(input.getAttribute('min'), NaN); 40 return (!isNaN(min) && min > 0) ? min : NaN; 41 } 42 43 function stepValue(input, dir) { 44 var stepSize = toNum(input.getAttribute('step'), 1) || 1; 45 var min = toNum(input.getAttribute('min'), NaN); 46 var max = toNum(input.getAttribute('max'), NaN); 47 var raw = String(input.value); 48 var hasVal = raw.trim() !== '' && !isNaN(parseFloat(raw)); 49 var next; 50 if (hasVal) { 51 next = parseFloat(raw) + dir * stepSize; 52 } else { 53 // Empty: the first press lands ON the starting value, never a step 54 // PAST it â so "+" on an empty min:1 field shows 1, not 2. Optional 55 // fields (fieldStart = NaN): "+" adds one unit, "â" goes to the floor. 56 var start = fieldStart(input); 57 if (!isNaN(start)) next = start; 58 else next = (dir > 0) ? stepSize : (isNaN(min) ? 0 : min); 59 } 60 if (!isNaN(min) && next < min) next = min; 61 if (!isNaN(max) && next > max) next = max; 62 if (hasVal && next === parseFloat(raw)) return; 63 input.value = String(next); 64 // Re-dispatch so dependent widgets (repricing, validity) react. 65 input.dispatchEvent(new Event('input', { bubbles: true })); 66 input.dispatchEvent(new Event('change', { bubbles: true })); 67 } 68 69 function button(dir) { 70 var b = document.createElement('button'); 71 b.type = 'button'; 72 b.className = 'btn btn-outline-secondary gc-stepper-btn gc-stepper-' + (dir < 0 ? 'down' : 'up'); 73 // FontAwesome glyph (core-enqueued site-wide) so the buttons match the 74 // rest of the theme's icon-based nav/controls. Icon is decorative â the 75 // button carries the accessible label. 76 var icon = document.createElement('i'); 77 icon.className = dir < 0 ? 'fa fa-minus' : 'fa fa-plus'; 78 icon.setAttribute('aria-hidden', 'true'); 79 b.appendChild(icon); 80 b.setAttribute('aria-label', dir < 0 ? (i18n.dec || 'Decrease') : (i18n.inc || 'Increase')); 81 b.tabIndex = -1; // keyboard users use the input's native arrows 82 return b; 83 } 84 85 function enhance(input) { 86 if (!input || input.getAttribute(READY_ATTR) || input.classList.contains('gc-no-stepper')) return; 87 var parent = input.parentNode; 88 if (!parent) return; 89 input.setAttribute(READY_ATTR, '1'); 90 91 var minus = button(-1); 92 var plus = button(1); 93 94 if (parent.classList && parent.classList.contains('input-group')) { 95 // The input already lives in a Bootstrap input-group â e.g. a field 96 // rendered with a unit suffix (`.input-group-text` showing m/kg/cm). 97 // Inject the buttons INTO that group around the input instead of 98 // nesting a second input-group, so the +/- and the suffix share one 99 // clean row ([-] value [+] unit) rather than fighting over it. 100 parent.classList.add('gc-stepper-group'); 101 parent.insertBefore(minus, input); 102 parent.insertBefore(plus, input.nextSibling); // after input, before any suffix 103 // The host (e.g. CF7's inline .wpcf7-form-control-wrap span) still 104 // needs promoting to block so the group can size to the column. 105 if (parent.parentNode && parent.parentNode.tagName === 'SPAN') { 106 parent.parentNode.classList.add('gc-stepper-host'); 107 } 108 } else { 109 var group = document.createElement('div'); 110 group.className = 'input-group gc-stepper-group'; 111 parent.insertBefore(group, input); 112 group.appendChild(minus); 113 group.appendChild(input); // moves the input; id/name/listeners preserved 114 group.appendChild(plus); 115 // CF7 (and some templates) wrap the input in an inline <span> 116 // (.wpcf7-form-control-wrap). An inline host shrinks to content, so 117 // the full-width group can't size to the column â promote it to block. 118 if (parent.tagName === 'SPAN') parent.classList.add('gc-stepper-host'); 119 } 120 input.classList.add('gc-stepper-input', 'text-center'); 121 122 minus.addEventListener('click', function () { stepValue(input, -1); }); 123 plus.addEventListener('click', function () { stepValue(input, 1); }); 124 125 // Show the field's starting value on first load: its default, or a 126 // positive required min (fieldStart). Optional fields stay blank. No 127 // event dispatch â this is the field's initial state, not an edit. 128 if (String(input.value).trim() === '') { 129 var start = fieldStart(input);
130 if (!isNaN(start)) input.value = String(start); 131 } 132 } 133 134 function scan(root) { 135 var scope = root && root.querySelectorAll ? root : document; 136 Array.prototype.forEach.call(scope.querySelectorAll(SELECTOR), enhance); 137 } 138 139 function ready(fn) { 140 if (document.readyState !== 'loading') { fn(); } 141 else { document.addEventListener('DOMContentLoaded', fn); } 142 } 143 144 ready(function () { 145 scan(document); 146 // Re-enhance inputs injected after load (variation-table reprice, 147 // cart re-render, step-2 modal, â¦). 148 if (window.MutationObserver && document.body) { 149 new MutationObserver(function (mutations) { 150 for (var i = 0; i < mutations.length; i++) { 151 var added = mutations[i].addedNodes; 152 for (var j = 0; j < added.length; j++) { 153 var node = added[j]; 154 if (node.nodeType !== 1) continue; 155 if (node.matches && node.matches(SELECTOR)) enhance(node); 156 if (node.querySelectorAll) scan(node); 157 } 158 } 159 }).observe(document.body, { childList: true, subtree: true }); 160 } 161 }); 162 163 // Explicit hook for scripts that want to enhance a freshly-built region. 164 window.gcInitSteppers = scan; 165})();
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.