PluginProbe
Booking Calendar / 11.2
Booking Calendar v11.2
11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 10.11.2 All 203 releases
booking / includes / __js / admin / slider_groups / wpbc_range_groups.js

wpbc_range_groups.js in Booking Calendar 11.2, at includes/__js/admin/slider_groups/wpbc_range_groups.js

278 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* globals window, document */
2 /**
3 * WPBC Slider Range Groups
4 *
5 * Universal, dependency-free controller that keeps a "range + number" pair in sync:
6 * - number input (data-wpbc_slider_range_value)
7 * - range slider (data-wpbc_slider_range_range)
8 * - writer input (data-wpbc_slider_range_writer) [optional]
9 *
10 * If writer exists: number/slider update writer and emit 'input' on writer (bubbles).
11 * If writer is missing: emits 'input' on the number input.
12 * If writer changes externally: updates number/slider.
13 *
14 * Markup expectations (minimal):
15 * <div class="wpbc_slider_range_group">
16 * <input type="number" data-wpbc_slider_range_value>
17 * <input type="range" data-wpbc_slider_range_range>
18 * <!-- optional -->
19 * <input type="text" data-wpbc_slider_range_writer style="display:none;">
20 * </div>
21 *
22 * Performance notes:
23 * - MutationObserver is DISABLED by default.
24 * - If your UI re-renders and inserts new groups dynamically, call:
25 * WPBC_Slider_Range_AutoInit(); OR instance.refresh();
26 * Or enable observer via: new WPBC_Slider_Range_Groups(root, { enable_observer:true }).init();
27 *
28 * Public API (instance methods):
29 * - init(), destroy(), refresh()
30 *
31 * @version 2026-01-25
32 * @since 2026-01-25
33 * @file ../includes/__js/admin/slider_groups/wpbc_range_groups.js
34 */
35 (function (w, d) {
36 'use strict';
37
38 // -------------------------------------------------------------------------------------------------
39 // Helpers
40 // -------------------------------------------------------------------------------------------------
41 function clamp_num(v, min, max) {
42 if (typeof min === 'number' && !isNaN(min)) v = Math.max(min, v);
43 if (typeof max === 'number' && !isNaN(max)) v = Math.min(max, v);
44 return v;
45 }
46
47 function parse_float(v) {
48 var n = parseFloat(v);
49 return isNaN(n) ? null : n;
50 }
51
52 function emit_input(el) {
53 if (!el) return;
54 el.dispatchEvent(new Event('input', { bubbles: true }));
55 }
56
57 // -------------------------------------------------------------------------------------------------
58 // Controller
59 // -------------------------------------------------------------------------------------------------
60 class WPBC_Slider_Range_Groups {
61
62 /**
63 * @param {HTMLElement|string} root_el Container (or selector). If omitted, uses document.
64 * @param {Object} [opts={}]
65 */
66 constructor(root_el, opts) {
67 this.root = root_el
68 ? ((typeof root_el === 'string') ? d.querySelector(root_el) : root_el)
69 : d;
70
71 this.opts = Object.assign({
72 // Strict selectors (NO backward compatibility).
73 group_selector : '.wpbc_slider_range_group',
74 value_selector : '[data-wpbc_slider_range_value]',
75 range_selector : '[data-wpbc_slider_range_range]',
76 writer_selector : '[data-wpbc_slider_range_writer]',
77
78 // Disabled by default for performance.
79 enable_observer : false,
80 observer_debounce_ms: 150
81 }, opts || {});
82
83 this._on_input = this._on_input.bind(this);
84 this._on_change = this._on_change.bind(this);
85
86 this._observer = null;
87 this._refresh_tmr = null;
88 }
89
90 init() {
91 if (!this.root) return this;
92
93 this.root.addEventListener('input', this._on_input, true);
94 this.root.addEventListener('change', this._on_change, true);
95
96 if (this.opts.enable_observer && w.MutationObserver) {
97 this._observer = new MutationObserver(() => { this._debounced_refresh(); });
98 this._observer.observe(this.root === d ? d.documentElement : this.root, { childList: true, subtree: true });
99 }
100
101 this.refresh();
102 return this;
103 }
104
105 destroy() {
106 if (!this.root) return;
107
108 this.root.removeEventListener('input', this._on_input, true);
109 this.root.removeEventListener('change', this._on_change, true);
110
111 if (this._observer) {
112 this._observer.disconnect();
113 this._observer = null;
114 }
115
116 if (this._refresh_tmr) {
117 clearTimeout(this._refresh_tmr);
118 this._refresh_tmr = null;
119 }
120 }
121
122 refresh() {
123 if (!this.root) return;
124
125 var scope = (this.root === d ? d : this.root);
126 var groups = Array.prototype.slice.call(scope.querySelectorAll(this.opts.group_selector));
127
128 for (var i = 0; i < groups.length; i++) {
129 this._sync_from_writer(groups[i]);
130 this._clamp_to_range(groups[i]);
131 }
132 }
133
134 // -------------------------------------------------------------------------------------------------
135 // Internal
136 // -------------------------------------------------------------------------------------------------
137 _debounced_refresh() {
138 if (this._refresh_tmr) clearTimeout(this._refresh_tmr);
139 this._refresh_tmr = setTimeout(() => {
140 this._refresh_tmr = null;
141 this.refresh();
142 }, Number(this.opts.observer_debounce_ms) || 0);
143 }
144
145 _find_group(el) {
146 return (el && el.closest) ? el.closest(this.opts.group_selector) : null;
147 }
148
149 _get_parts(group) {
150 if (!group) return null;
151 return {
152 group : group,
153 num : group.querySelector(this.opts.value_selector),
154 range : group.querySelector(this.opts.range_selector),
155 writer: group.querySelector(this.opts.writer_selector)
156 };
157 }
158
159 _write(parts, value, emit) {
160 if (!parts) return;
161
162 if (parts.writer) {
163 parts.writer.__wpbc_slider_range_internal = true;
164 parts.writer.value = String(value);
165 if (emit) emit_input(parts.writer);
166 parts.writer.__wpbc_slider_range_internal = false;
167 } else if (parts.num) {
168 // If writer is missing, at least notify via number input.
169 if (emit) emit_input(parts.num);
170 }
171 }
172
173 _sync_from_writer(group) {
174 var parts = this._get_parts(group);
175 if (!parts || !parts.writer) return;
176
177 var raw = String(parts.writer.value || '').trim();
178 if (!raw) return;
179
180 if (parts.num) parts.num.value = raw;
181 if (parts.range) parts.range.value = raw;
182 }
183
184 _clamp_to_range(group) {
185 var parts = this._get_parts(group);
186 if (!parts || !parts.range || !parts.num) return;
187
188 var v = parse_float(parts.num.value);
189 if (v == null) return;
190
191 var min = Number(parts.range.min);
192 var max = Number(parts.range.max);
193 var vv = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
194
195 if (String(vv) !== parts.num.value) parts.num.value = String(vv);
196 parts.range.value = String(vv);
197 }
198
199 _on_input(ev) {
200 var t = ev.target;
201 if (!t) return;
202
203 var group = this._find_group(t);
204 if (!group) return;
205
206 var parts = this._get_parts(group);
207 if (!parts) return;
208
209 // Writer changed externally -> update UI.
210 if (parts.writer && t === parts.writer) {
211 if (t.__wpbc_slider_range_internal) return;
212 this._sync_from_writer(group);
213 this._clamp_to_range(group);
214 return;
215 }
216
217 // Range moved -> update number + writer.
218 if (t.matches && t.matches(this.opts.range_selector)) {
219 if (parts.num) parts.num.value = t.value;
220 this._write(parts, t.value, /*emit*/ true);
221 return;
222 }
223
224 // Number typed -> update range + writer (clamp by slider bounds).
225 if (t.matches && t.matches(this.opts.value_selector)) {
226 if (parts.range) {
227 var v = parse_float(t.value);
228 if (v != null) {
229 var min = Number(parts.range.min);
230 var max = Number(parts.range.max);
231 v = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
232
233 parts.range.value = String(v);
234 if (String(v) !== t.value) t.value = String(v);
235 }
236 }
237 this._write(parts, t.value, /*emit*/ true);
238 }
239 }
240
241 _on_change(ev) {
242 // No special "change" handling needed currently; kept for symmetry/future.
243 }
244 }
245
246 // -------------------------------------------------------------------------------------------------
247 // Auto-init
248 // -------------------------------------------------------------------------------------------------
249 function wpbc_slider_range_groups__auto_init() {
250 var ROOT = '.wpbc_slider_range_groups';
251 var nodes = Array.prototype.slice.call(d.querySelectorAll(ROOT))
252 .filter(function (n) { return !n.parentElement || !n.parentElement.closest(ROOT); });
253
254 if (!nodes.length) {
255 if (!d.__wpbc_slider_range_groups_global_instance) {
256 d.__wpbc_slider_range_groups_global_instance = new WPBC_Slider_Range_Groups(d).init();
257 }
258 return;
259 }
260
261 nodes.forEach(function (node) {
262 if (node.__wpbc_slider_range_groups_instance) return;
263 node.__wpbc_slider_range_groups_instance = new WPBC_Slider_Range_Groups(node).init();
264 });
265 }
266
267 // Export globals.
268 w.WPBC_Slider_Range_Groups = WPBC_Slider_Range_Groups;
269 w.WPBC_Slider_Range_AutoInit = wpbc_slider_range_groups__auto_init;
270
271 if (d.readyState === 'loading') {
272 d.addEventListener('DOMContentLoaded', wpbc_slider_range_groups__auto_init, { once: true });
273 } else {
274 wpbc_slider_range_groups__auto_init();
275 }
276
277 })(window, document);
278