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_len_groups.js

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

407 lines 13.1 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 Length Groups
4 *
5 * Universal, dependency-free controller that keeps a "length" control in sync:
6 * - number input (data-wpbc_slider_len_value)
7 * - unit select (data-wpbc_slider_len_unit)
8 * - range slider (data-wpbc_slider_len_range)
9 * - writer input (data-wpbc_slider_len_writer) [optional but recommended]
10 *
11 * The "writer" stores the combined value like: "100%", "420px", "12.5rem".
12 * When number/unit/slider change -> writer updates and emits 'input' (bubbles).
13 * When writer is changed externally (apply-from-JSON, etc) -> UI updates.
14 *
15 * Markup expectations (minimal):
16 * <div class="wpbc_slider_len_group"
17 * data-wpbc_slider_len_bounds_map='{"%":{"min":30,"max":100,"step":1},"px":{"min":300,"max":2000,"step":10}}'
18 * data-wpbc_slider_len_default_unit="%">
19 * <input type="number" data-wpbc_slider_len_value>
20 * <select data-wpbc_slider_len_unit>...</select>
21 * <input type="range" data-wpbc_slider_len_range>
22 * <input type="text" data-wpbc_slider_len_writer style="display:none;">
23 * </div>
24 *
25 * Performance notes:
26 * - MutationObserver is DISABLED by default (prevents performance issues).
27 * - If your UI re-renders and inserts new groups dynamically, call:
28 * WPBC_Slider_Len_AutoInit(); OR instance.refresh();
29 * Or enable observer via: new WPBC_Slider_Len_Groups(root, { enable_observer:true }).init();
30 *
31 * Public API (instance methods):
32 * - init(), destroy(), refresh()
33 *
34 * @version 2026-01-25
35 * @since 2026-01-25
36 * @file ../includes/__js/admin/slider_groups/wpbc_len_groups.js
37 */
38 (function (w, d) {
39 'use strict';
40
41 // -------------------------------------------------------------------------------------------------
42 // Helpers
43 // -------------------------------------------------------------------------------------------------
44 function clamp_num(v, min, max) {
45 if (typeof min === 'number' && !isNaN(min)) v = Math.max(min, v);
46 if (typeof max === 'number' && !isNaN(max)) v = Math.min(max, v);
47 return v;
48 }
49
50 function parse_float(v) {
51 var n = parseFloat(v);
52 return isNaN(n) ? null : n;
53 }
54
55 function safe_json_parse(str) {
56 try {
57 return JSON.parse(str);
58 } catch (e) {
59 return null;
60 }
61 }
62
63 function parse_len_combined(raw, default_unit) {
64 var s = (raw == null) ? '' : String(raw).trim();
65 if (!s) return { num: '', unit: default_unit || '%' };
66
67 var m = s.match(/^\s*([\-]?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i);
68 if (!m) {
69 // If it's not parseable, treat as number and keep default unit.
70 return { num: s, unit: default_unit || '%' };
71 }
72
73 var num = m[1] ? String(m[1]) : '';
74 var unit = m[2] ? String(m[2]) : '';
75 if (!unit) unit = default_unit || '%';
76
77 return { num: num, unit: unit };
78 }
79
80 function build_combined(num, unit) {
81 if (num == null || String(num).trim() === '') return '';
82 return String(num) + String(unit || '');
83 }
84
85 function emit_input(el) {
86 if (!el) return;
87 el.dispatchEvent(new Event('input', { bubbles: true }));
88 }
89
90 // -------------------------------------------------------------------------------------------------
91 // Controller
92 // -------------------------------------------------------------------------------------------------
93 class WPBC_Slider_Len_Groups {
94
95 /**
96 * @param {HTMLElement|string} root_el Container (or selector). If omitted, uses document.
97 * @param {Object} [opts={}]
98 */
99 constructor(root_el, opts) {
100 this.root = root_el
101 ? ((typeof root_el === 'string') ? d.querySelector(root_el) : root_el)
102 : d;
103
104 this.opts = Object.assign({
105 // Strict selectors (NO backward compatibility).
106 group_selector : '.wpbc_slider_len_group',
107 value_selector : '[data-wpbc_slider_len_value]',
108 unit_selector : '[data-wpbc_slider_len_unit]',
109 range_selector : '[data-wpbc_slider_len_range]',
110 writer_selector : '[data-wpbc_slider_len_writer]',
111
112 default_unit : '%',
113
114 fallback_bounds : {
115 'px' : { min: 0, max: 512, step: 1 },
116 '%' : { min: 0, max: 100, step: 1 },
117 'rem': { min: 0, max: 10, step: 0.1 },
118 'em' : { min: 0, max: 10, step: 0.1 }
119 },
120
121 // Disabled by default for performance.
122 enable_observer : false,
123 observer_debounce_ms: 150
124 }, opts || {});
125
126 this._on_input = this._on_input.bind(this);
127 this._on_change = this._on_change.bind(this);
128
129 this._bounds_cache = new WeakMap(); // group -> bounds_map_object
130 this._observer = null;
131 this._refresh_tmr = null;
132 }
133
134 init() {
135 if (!this.root) return this;
136
137 this.root.addEventListener('input', this._on_input, true);
138 this.root.addEventListener('change', this._on_change, true);
139
140 if (this.opts.enable_observer && w.MutationObserver) {
141 this._observer = new MutationObserver(() => { this._debounced_refresh(); });
142 this._observer.observe(this.root === d ? d.documentElement : this.root, { childList: true, subtree: true });
143 }
144
145 this.refresh();
146 return this;
147 }
148
149 destroy() {
150 if (!this.root) return;
151
152 this.root.removeEventListener('input', this._on_input, true);
153 this.root.removeEventListener('change', this._on_change, true);
154
155 if (this._observer) {
156 this._observer.disconnect();
157 this._observer = null;
158 }
159
160 if (this._refresh_tmr) {
161 clearTimeout(this._refresh_tmr);
162 this._refresh_tmr = null;
163 }
164 }
165
166 refresh() {
167 if (!this.root) return;
168
169 var scope = (this.root === d ? d : this.root);
170 var groups = Array.prototype.slice.call(scope.querySelectorAll(this.opts.group_selector));
171
172 for (var i = 0; i < groups.length; i++) {
173 this._sync_group_from_writer(groups[i]);
174 this._apply_bounds_for_current_unit(groups[i]);
175 }
176 }
177
178 // -------------------------------------------------------------------------------------------------
179 // Internal
180 // -------------------------------------------------------------------------------------------------
181 _debounced_refresh() {
182 if (this._refresh_tmr) clearTimeout(this._refresh_tmr);
183 this._refresh_tmr = setTimeout(() => {
184 this._refresh_tmr = null;
185 this.refresh();
186 }, Number(this.opts.observer_debounce_ms) || 0);
187 }
188
189 _find_group(el) {
190 return (el && el.closest) ? el.closest(this.opts.group_selector) : null;
191 }
192
193 _get_parts(group) {
194 if (!group) return null;
195 return {
196 group : group,
197 num : group.querySelector(this.opts.value_selector),
198 unit : group.querySelector(this.opts.unit_selector),
199 range : group.querySelector(this.opts.range_selector),
200 writer: group.querySelector(this.opts.writer_selector)
201 };
202 }
203
204 _get_default_unit(group) {
205 var du = (group && group.getAttribute)
206 ? group.getAttribute('data-wpbc_slider_len_default_unit')
207 : '';
208 return du ? String(du) : this.opts.default_unit;
209 }
210
211 _get_bounds_map(group) {
212 if (!group) return null;
213 if (this._bounds_cache.has(group)) {
214 return this._bounds_cache.get(group);
215 }
216
217 var raw = group.getAttribute('data-wpbc_slider_len_bounds_map');
218 var map = raw ? safe_json_parse(raw) : null;
219 if (!map || typeof map !== 'object') map = null;
220
221 this._bounds_cache.set(group, map);
222 return map;
223 }
224
225 _get_bounds_for_unit(group, unit) {
226 var map = this._get_bounds_map(group);
227 if (map && unit && map[unit]) {
228 return map[unit];
229 }
230 return this.opts.fallback_bounds[unit] || this.opts.fallback_bounds['px'];
231 }
232
233 _apply_bounds(parts, bounds) {
234 if (!parts || !bounds) return;
235
236 var min = (bounds.min != null) ? Number(bounds.min) : null;
237 var max = (bounds.max != null) ? Number(bounds.max) : null;
238 var step = (bounds.step != null) ? Number(bounds.step) : null;
239
240 if (parts.range) {
241 if (!isNaN(min)) parts.range.min = String(min);
242 if (!isNaN(max)) parts.range.max = String(max);
243 if (!isNaN(step)) parts.range.step = String(step);
244 }
245 if (parts.num) {
246 if (!isNaN(min)) parts.num.min = String(min);
247 if (!isNaN(max)) parts.num.max = String(max);
248 if (!isNaN(step)) parts.num.step = String(step);
249 }
250 }
251
252 _apply_bounds_for_current_unit(group) {
253 var parts = this._get_parts(group);
254 if (!parts || !parts.unit) return;
255
256 var unit = parts.unit.value || this._get_default_unit(group);
257 var b = this._get_bounds_for_unit(group, unit);
258
259 this._apply_bounds(parts, b);
260
261 // Clamp current value to new bounds.
262 var v = parse_float(parts.num && parts.num.value ? parts.num.value : (parts.range ? parts.range.value : ''));
263 if (v == null) return;
264
265 var min = (b && b.min != null) ? Number(b.min) : null;
266 var max = (b && b.max != null) ? Number(b.max) : null;
267 v = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
268
269 if (parts.num) parts.num.value = String(v);
270 if (parts.range) parts.range.value = String(v);
271
272 this._write_combined(parts, String(v), unit, /*emit*/ false);
273 }
274
275 _write_combined(parts, num, unit, emit) {
276 if (!parts) return;
277
278 var combined = build_combined(num, unit);
279
280 if (parts.writer) {
281 // Avoid recursion: mark as internal write.
282 parts.writer.__wpbc_slider_len_internal = true;
283 parts.writer.value = combined;
284 if (emit) emit_input(parts.writer);
285 parts.writer.__wpbc_slider_len_internal = false;
286 } else if (parts.num) {
287 // If writer is missing, at least notify via number input.
288 if (emit) emit_input(parts.num);
289 }
290 }
291
292 _sync_group_from_writer(group) {
293 var parts = this._get_parts(group);
294 if (!parts || !parts.writer) return;
295
296 var raw = String(parts.writer.value || '').trim();
297 if (!raw) return;
298
299 var du = this._get_default_unit(group);
300 var p = parse_len_combined(raw, du);
301
302 if (parts.unit) parts.unit.value = p.unit;
303 if (parts.num) parts.num.value = p.num;
304 if (parts.range) parts.range.value = p.num;
305 }
306
307 _on_input(ev) {
308 var t = ev.target;
309 if (!t) return;
310
311 var group = this._find_group(t);
312 if (!group) return;
313
314 var parts = this._get_parts(group);
315 if (!parts) return;
316
317 // Writer changed externally -> update UI.
318 if (parts.writer && t === parts.writer) {
319 if (t.__wpbc_slider_len_internal) return;
320 this._sync_group_from_writer(group);
321 this._apply_bounds_for_current_unit(group);
322 return;
323 }
324
325 // Slider moved -> update number + writer.
326 if (t.matches && t.matches(this.opts.range_selector)) {
327 if (parts.num) parts.num.value = t.value;
328
329 var unit = (parts.unit && parts.unit.value) ? parts.unit.value : this._get_default_unit(group);
330 this._write_combined(parts, t.value, unit, /*emit*/ true);
331 return;
332 }
333
334 // Number typed -> update slider + writer (clamp if slider has bounds).
335 if (t.matches && t.matches(this.opts.value_selector)) {
336 var v = parse_float(t.value);
337
338 if (v != null && parts.range) {
339 var rmin = Number(parts.range.min);
340 var rmax = Number(parts.range.max);
341 v = clamp_num(v, isNaN(rmin) ? null : rmin, isNaN(rmax) ? null : rmax);
342
343 parts.range.value = String(v);
344 if (String(v) !== t.value) t.value = String(v);
345 }
346
347 var unit2 = (parts.unit && parts.unit.value) ? parts.unit.value : this._get_default_unit(group);
348 this._write_combined(parts, t.value, unit2, /*emit*/ true);
349 }
350 }
351
352 _on_change(ev) {
353 var t = ev.target;
354 if (!t) return;
355
356 var group = this._find_group(t);
357 if (!group) return;
358
359 var parts = this._get_parts(group);
360 if (!parts) return;
361
362 // Unit changed -> update bounds + writer.
363 if (t.matches && t.matches(this.opts.unit_selector)) {
364 this._apply_bounds_for_current_unit(group);
365
366 var num = parts.num ? parts.num.value : (parts.range ? parts.range.value : '');
367 var unit = t.value || this._get_default_unit(group);
368 this._write_combined(parts, num, unit, /*emit*/ true);
369 }
370 }
371 }
372
373 // -------------------------------------------------------------------------------------------------
374 // Auto-init
375 // -------------------------------------------------------------------------------------------------
376 function wpbc_slider_len_groups__auto_init() {
377 var ROOT = '.wpbc_slider_len_groups';
378 var nodes = Array.prototype.slice.call(d.querySelectorAll(ROOT))
379 .filter(function (n) { return !n.parentElement || !n.parentElement.closest(ROOT); });
380
381 // If no explicit containers, install a single document-root instance.
382 if (!nodes.length) {
383 if (!d.__wpbc_slider_len_groups_global_instance) {
384 d.__wpbc_slider_len_groups_global_instance = new WPBC_Slider_Len_Groups(d).init();
385 }
386 return;
387 }
388
389 nodes.forEach(function (node) {
390 if (node.__wpbc_slider_len_groups_instance) return;
391 node.__wpbc_slider_len_groups_instance = new WPBC_Slider_Len_Groups(node).init();
392 });
393 }
394
395 // Export globals (manual control if needed).
396 w.WPBC_Slider_Len_Groups = WPBC_Slider_Len_Groups;
397 w.WPBC_Slider_Len_AutoInit = wpbc_slider_len_groups__auto_init;
398
399 // DOM-ready auto init.
400 if (d.readyState === 'loading') {
401 d.addEventListener('DOMContentLoaded', wpbc_slider_len_groups__auto_init, { once: true });
402 } else {
403 wpbc_slider_len_groups__auto_init();
404 }
405
406 })(window, document);
407