PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
11.8.4 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 All 204 releases
booking / includes / page-form-builder / field-packs / divider / _src / divider.js

divider.js in Booking Calendar 11.7, at includes/page-form-builder/field-packs/divider/_src/divider.js

477 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // File: /includes/page-form-builder/field-packs/divider/_out/divider.js
2 (function (w) {
3 'use strict';
4
5 /** @type {any} */
6 var Core = w.WPBC_BFB_Core || {};
7 var registry = Core.WPBC_BFB_Field_Renderer_Registry;
8 var Base = Core.WPBC_BFB_Field_Base;
9
10 if (!registry || typeof registry.register !== 'function' || !Base) {
11 _wpbc?.dev?.error?.('wpbc_bfb_field_divider', 'Core registry/base missing');
12 return;
13 }
14
15 /**
16 * WPBC BFB: Field Renderer for "divider" (Schema-driven)
17 * - Inspector is rendered by Factory (from PHP schema).
18 * - No wp.template usage for preview.
19 * - Renders either a horizontal <hr> or a vertical <div> with border-left.
20 */
21 class wpbc_bfb_field_divider extends Base {
22
23 /**
24 * Return default props for "divider" field.
25 * Must stay in sync with PHP schema defaults.
26 *
27 * @returns {{type:string,orientation:'horizontal'|'vertical',line_style:'solid'|'dashed'|'dotted',thickness_px:number,length:string,align:string,color:string,margin_top_px:number,margin_bottom_px:number,margin_left_px:number,margin_right_px:number,cssclass_extra:string,name:string,html_id:string,help:string,usage_key:string}}
28 */
29 static get_defaults() {
30 return {
31 type : 'divider',
32 orientation : 'horizontal', // 'horizontal' | 'vertical'
33 line_style : 'solid', // 'solid' | 'dashed' | 'dotted'
34 thickness_px : 1,
35 length : '100%', // width for horizontal, height for vertical
36 align : 'center', // 'left' | 'center' | 'right' (or vertical equivalents; normalized later)
37 color : '#e0e0e0',
38 margin_top_px : 2,
39 margin_bottom_px: 2,
40 margin_left_px : 2,
41 margin_right_px : 2,
42 cssclass_extra : '',
43 name : '',
44 html_id : '',
45 help : '',
46 usage_key : 'divider'
47 };
48 }
49
50 /**
51 * Normalize align tokens for the current orientation.
52 * - For horizontal: accepts left|center|right or top|middle|bottom → maps to left|center|right.
53 * - For vertical : accepts top|middle|bottom or left|center|right → maps to top|middle|bottom.
54 *
55 * @param {'horizontal'|'vertical'} orientation
56 * @param {string|undefined|null} align_raw
57 * @returns {'left'|'center'|'right'|'top'|'middle'|'bottom'}
58 */
59 static normalize_align(orientation, align_raw) {
60 const a = String(align_raw ?? '').toLowerCase();
61 const H = { top: 'left', middle: 'center', bottom: 'right' };
62 const V = { left: 'top', center: 'middle', right: 'bottom' };
63 const isV = orientation === 'vertical';
64 if (isV) return (a in V) ? /** @type any */(V)[a] : (['top','middle','bottom'].includes(a) ? /** @type any */(a) : 'middle');
65 return (a in H) ? /** @type any */(H)[a] : (['left','center','right'].includes(a) ? /** @type any */(a) : 'center');
66 }
67
68 /**
69 * Render the preview markup into the field element.
70 *
71 * @param {HTMLElement} el Field root element inside the canvas.
72 * @param {Object} data Field props (already normalized by schema).
73 * @param {{builder?:any, sanit?:any}} [ctx] Context object.
74 * @returns {void}
75 */
76 static render(el, data, ctx) {
77 if (!el) return;
78
79 // Normalize against defaults first.
80 const d = this.normalize_data(data);
81
82 // ----- Core sanitize helpers (static) -----
83 const eh = (v) => Core.WPBC_BFB_Sanitize.escape_html(v);
84 const sid = (v) => Core.WPBC_BFB_Sanitize.sanitize_html_id(v);
85 const sname = (v) => Core.WPBC_BFB_Sanitize.sanitize_html_name(v);
86 const sclass = (v) => Core.WPBC_BFB_Sanitize.sanitize_css_classlist(v);
87
88 /**
89 * @param {any} v
90 * @param {number} def
91 * @param {number} [min]
92 * @param {number} [max]
93 * @returns {number}
94 */
95 const to_int = (v, def, min, max) => {
96 let n = parseInt(v, 10);
97 if (isNaN(n)) n = def;
98 if (typeof min === 'number' && n < min) n = min;
99 if (typeof max === 'number' && n > max) n = max;
100 return n;
101 };
102
103 /**
104 * @param {any} v
105 * @param {string} def
106 * @returns {string}
107 */
108 const to_str = (v, def) => (v === undefined || v === null) ? def : String(v);
109
110 // Coerce props.
111 /** @type {'horizontal'|'vertical'} */
112 const orientation = (to_str(d.orientation, 'horizontal') === 'vertical') ? 'vertical' : 'horizontal';
113 /** @type {'solid'|'dashed'|'dotted'} */
114 const line_style = ({ solid:1, dashed:1, dotted:1 }[ to_str(d.line_style, 'solid') ]) ? d.line_style : 'solid';
115 const align_norm = wpbc_bfb_field_divider.normalize_align(orientation, to_str(d.align, orientation === 'vertical' ? 'middle' : 'center')); // accept both sets, then normalize to the active orientation.
116 const thickness_px = to_int(d.thickness_px, 1, 1, 20);
117 const length_val = Core.WPBC_BFB_Sanitize.sanitize_css_len(d.length, '100%');
118 const color_val = Core.WPBC_BFB_Sanitize.sanitize_hex_color(d.color, '#e0e0e0');
119
120 const m_top = to_int(d.margin_top_px, 8, 0, 200);
121 const m_bottom = to_int(d.margin_bottom_px, 8, 0, 200);
122 const m_left = to_int(d.margin_left_px, 8, 0, 200);
123 const m_right = to_int(d.margin_right_px, 8, 0, 200);
124
125 const html_id = d.html_id ? sid(String(d.html_id)) : '';
126 const name_val = d.name ? sname(String(d.name)) : '';
127 const cls_extra = sclass(String(d.cssclass_extra || ''));
128
129 // Persist useful props on dataset (do not mutate wrapper classes directly).
130 if (el.dataset.orientation !== orientation) el.dataset.orientation = orientation;
131 if (el.dataset.line_style !== line_style) el.dataset.line_style = line_style;
132 if (String(el.dataset.thickness_px) !== String(thickness_px)) el.dataset.thickness_px = String(thickness_px);
133 if (el.dataset.length !== length_val) el.dataset.length = length_val;
134 if (el.dataset.align !== align_norm) el.dataset.align = align_norm;
135 if (el.dataset.color !== color_val) el.dataset.color = color_val;
136
137 if (String(el.dataset.margin_top_px) !== String(m_top)) el.dataset.margin_top_px = String(m_top);
138 if (String(el.dataset.margin_bottom_px) !== String(m_bottom)) el.dataset.margin_bottom_px = String(m_bottom);
139 if (String(el.dataset.margin_left_px) !== String(m_left)) el.dataset.margin_left_px = String(m_left);
140 if (String(el.dataset.margin_right_px) !== String(m_right)) el.dataset.margin_right_px = String(m_right);
141
142 if (el.dataset.cssclass_extra !== cls_extra) el.dataset.cssclass_extra = cls_extra;
143 if (el.dataset.html_id !== html_id) el.dataset.html_id = html_id;
144 if (el.dataset.name !== name_val) el.dataset.name = name_val;
145
146 // Attribute fragments for the line element.
147 const id_attr = html_id ? ` id="${eh(html_id)}"` : '';
148 const name_attr = name_val ? ` name="${eh(name_val)}"` : '';
149 const cls_attr = cls_extra ? ` class="${eh(cls_extra)}"` : '';
150
151 // Compute style for margins (outer wrapper).
152 const outer_style_attr = ` style="margin:${m_top}px ${m_right}px ${m_bottom}px ${m_left}px;"`;
153
154 // Build inner line markup based on orientation.
155 let line_html = '';
156 if (orientation === 'horizontal') {
157 // Horizontal: <hr> using border-top.
158 const ml = (align_norm === 'left') ? '0' : 'auto';
159 const mr = (align_norm === 'right') ? '0' : 'auto';
160 const hr_style = [
161 'border:none',
162 'height:0',
163 `border-top:${thickness_px}px ${line_style} ${color_val}`,
164 `width:${length_val}`,
165 `margin-left:${ml}`,
166 `margin-right:${mr}`
167 ].join(';');
168 line_html = `<hr${cls_attr}${id_attr}${name_attr} style="${hr_style}">`;
169 } else {
170 // Vertical: <div> using border-left and height.
171 let v_align = '';
172 v_align = (align_norm === 'top') ? 'position:absolute;top:0;' : v_align;
173 v_align = (align_norm === 'middle') ? 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);' : v_align;
174 v_align = (align_norm === 'bottom') ? 'position:absolute;bottom:0;' : v_align;
175
176 const vr_style = [
177 `border-left:${thickness_px}px ${line_style} ${color_val}`,
178 `height:${length_val}`,
179 `${v_align}`,
180 'padding-left:0'
181 ].join(';');
182 line_html = `<div${cls_attr}${id_attr}${name_attr} role="separator" aria-orientation="vertical" style="${vr_style}"></div>`;
183 }
184
185 // Optional help text below the line.
186 const help_html = d.help ? `<div class="wpbc_bfb__help">${eh(String(d.help))}</div>` : '';
187
188 // Render preview HTML.
189 el.innerHTML = `
190 <span class="wpbc_bfb__noaction wpbc_bfb__no-drag-zone" inert="">
191 <div class="wpbc_bfb__field-preview"${outer_style_attr}>
192 ${line_html}
193 </div>
194 ${help_html}
195 </span>
196 `;
197
198 // Overlay (handles/toolbars).
199 Core.UI?.WPBC_BFB_Overlay?.ensure?.(ctx?.builder, el);
200 }
201
202 /**
203 * Optional hook executed after field is dropped from the palette.
204 *
205 * @param {Object} data
206 * @param {HTMLElement} el
207 * @param {{palette_item?: HTMLElement}} [ctx]
208 * @returns {void}
209 */
210 static on_field_drop(data, el, ctx) {
211 // Keep base behavior (auto-name, etc.).
212 super.on_field_drop?.(data, el, ctx);
213
214 // Apply orientation from palette hint (data-orientation) once on drop.
215 try {
216 const src = ctx?.palette_item;
217 if (src) {
218 const ori = src.getAttribute('data-orientation');
219 if (ori === 'horizontal' || ori === 'vertical') {
220 el.dataset.orientation = ori;
221 }
222 }
223 } catch (e) {}
224 }
225 }
226
227 try {
228 registry.register('divider', wpbc_bfb_field_divider);
229 } catch (e) { _wpbc?.dev?.error?.('wpbc_bfb_field_divider.register', e); }
230
231
232 // -----------------------------------------------------------------------------
233 // Exporters – booking form + “Content of booking fields data”
234 // -----------------------------------------------------------------------------
235
236 /**
237 * Register booking form exporter for "divider".
238 * Mirrors the previous legacy export logic but now kept inside the pack.
239 */
240 function register_divider_form_exporter() {
241 if (!w.WPBC_BFB_Exporter || typeof w.WPBC_BFB_Exporter.register !== 'function') {
242 return;
243 }
244
245 w.WPBC_BFB_Exporter.register('divider', function (field, emit, extras) {
246 if (!field || typeof emit !== 'function') return;
247
248 const core = (extras && extras.core) || w.WPBC_BFB_Core || {};
249 const Sanit = core.WPBC_BFB_Sanitize || {};
250
251 const esc_html = Sanit.escape_html ? Sanit.escape_html.bind(Sanit) : (s) => String(s);
252 const cls_sanit = Sanit.sanitize_css_classlist ? Sanit.sanitize_css_classlist.bind(Sanit) : (s) => String(s);
253 const sid = Sanit.sanitize_html_id ? Sanit.sanitize_html_id.bind(Sanit) : (s) => String(s);
254 const sname = Sanit.sanitize_html_name ? Sanit.sanitize_html_name.bind(Sanit) : (s) => String(s);
255
256 /**
257 * @param {any} v
258 * @param {number} def
259 * @param {number} [min]
260 * @param {number} [max]
261 * @returns {number}
262 */
263 const to_int = (v, def, min, max) => {
264 let n = parseInt(v, 10);
265 if (isNaN(n)) n = def;
266 if (typeof min === 'number' && n < min) n = min;
267 if (typeof max === 'number' && n > max) n = max;
268 return n;
269 };
270
271 /**
272 * @param {any} v
273 * @param {string} def
274 * @returns {string}
275 */
276 const to_str = (v, def) => (v == null || v === '') ? def : String(v);
277
278 // Normalize against defaults (schema-driven).
279 const d = wpbc_bfb_field_divider.normalize_data(field);
280
281 // ---- read & sanitize props (keep in sync with preview/schema) ----
282 /** @type {'horizontal'|'vertical'} */
283 const orientation = (to_str(d.orientation, 'horizontal') === 'vertical') ? 'vertical' : 'horizontal';
284 const line_style_in = to_str(d.line_style, 'solid');
285 /** @type {'solid'|'dashed'|'dotted'} */
286 const line_style = ({ solid:1, dashed:1, dotted:1 })[ line_style_in ] ? line_style_in : 'solid';
287
288 const thickness_px = to_int(d.thickness_px, 1, 1, 20);
289 const length_val = to_str(d.length, '100%');
290 const align_norm = wpbc_bfb_field_divider.normalize_align(
291 orientation,
292 to_str(d.align, orientation === 'vertical' ? 'middle' : 'center')
293 ); // accept both sets; normalize to orientation-appropriate token.
294 const color_val = to_str(d.color, '#e0e0e0');
295
296 const mt = to_int(d.margin_top_px, 8, 0, 200);
297 const mb = to_int(d.margin_bottom_px, 8, 0, 200);
298 const ml = to_int(d.margin_left_px, 8, 0, 200);
299 const mr = to_int(d.margin_right_px, 8, 0, 200);
300
301 const html_id = d.html_id ? sid(String(d.html_id)) : '';
302 const name_val = d.name ? sname(String(d.name)) : '';
303 const cls_extra = cls_sanit(String(d.cssclass_extra || d.cssclass || d.class || ''));
304
305 const id_attr = html_id ? ` id="${esc_html(html_id)}"` : '';
306 const name_attr = name_val ? ` name="${esc_html(name_val)}"` : '';
307
308 // Wrap styles (margins applied on the wrapper, not the line itself).
309 // In vertical mode we also make the wrapper flex and stretch so height:100% resolves.
310 const wrapper_styles = [
311 `margin:${mt}px ${mr}px ${mb}px ${ml}px`,
312 (orientation === 'vertical') ? 'display:flex' : '',
313 (orientation === 'vertical') ? 'align-self:stretch' : ''
314 ].filter(Boolean).join('; ');
315 const wrapper_attr = wrapper_styles ? ` style="${esc_html(wrapper_styles)}"` : '';
316
317 let line_html = '';
318 if (orientation === 'horizontal') {
319 // Horizontal divider via <hr> with border-top
320 const ml_auto = (align_norm === 'left') ? '0' : 'auto';
321 const mr_auto = (align_norm === 'right') ? '0' : 'auto';
322 const hr_styles = [
323 'border:none',
324 'height:0',
325 `border-top:${thickness_px}px ${line_style} ${color_val}`,
326 `width:${length_val}`,
327 `margin-left:${ml_auto}`,
328 `margin-right:${mr_auto}`
329 ].join('; ');
330 line_html = `<hr${id_attr}${name_attr} class="wpbc_bfb_divider wpbc_bfb_divider--h${cls_extra ? ' ' + esc_html(cls_extra) : ''}" style="${esc_html(hr_styles)}">`;
331 } else {
332 // Vertical divider via <div> with border-left; height = length
333 // (positioning handled client-side; export keeps the same inline style set)
334 // align_norm is 'top'|'middle'|'bottom' here.
335 let vr_styles_align = '';
336 vr_styles_align = (align_norm === 'top') ? 'position: absolute;top: 0;' : vr_styles_align;
337 vr_styles_align = (align_norm === 'middle') ? 'position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);' : vr_styles_align;
338 vr_styles_align = (align_norm === 'bottom') ? 'position: absolute;bottom:0;' : vr_styles_align;
339
340 const vr_styles = [
341 `border-left:${thickness_px}px ${line_style} ${color_val}`,
342 `height:${length_val}`,
343 'padding-left:0',
344 vr_styles_align
345 ].join('; ');
346
347 line_html = `<div${id_attr}${name_attr} class="wpbc_bfb_divider wpbc_bfb_divider--v${cls_extra ? ' ' + esc_html(cls_extra) : ''}" role="separator" aria-orientation="vertical" style="${esc_html(vr_styles)}"></div>`;
348 }
349
350 // Output: a small wrapper so CSS can target export safely (no builder-only classes).
351 emit(
352 `<div class="wpbc_bfb_divider_wrap" data-bfb-type="divider" data-orientation="${orientation}"${wrapper_attr}>${line_html}</div>`
353 );
354
355 // NOTE: Help text is appended by WPBC_BFB_Exporter.render_field_node() after this exporter runs.
356 });
357 }
358
359 /**
360 * Register empty content exporter for "divider" so it explicitly outputs nothing
361 * in “Content of booking fields data”.
362 */
363 function register_divider_content_exporter() {
364 if (!w.WPBC_BFB_ContentExporter || typeof w.WPBC_BFB_ContentExporter.register !== 'function') {
365 return;
366 }
367
368 // Divider is purely visual; nothing to show in content template.
369 w.WPBC_BFB_ContentExporter.register('divider', function (_field, _emit, _ctx) {
370 // Intentionally empty – explicit no-op for content export.
371 });
372 }
373
374 // Immediate registration if exporters are already loaded.
375 try {
376 register_divider_form_exporter();
377 } catch (e) { _wpbc?.dev?.error?.('wpbc_bfb_field_divider.exporter', e); }
378
379 try {
380 register_divider_content_exporter();
381 } catch (e) { _wpbc?.dev?.error?.('wpbc_bfb_field_divider.content_exporter', e); }
382
383 // Deferred registration when builder-exporter.js fires its ready events.
384 document.addEventListener('wpbc:bfb:exporter-ready', register_divider_form_exporter);
385 document.addEventListener('wpbc:bfb:content-exporter-ready', register_divider_content_exporter);
386
387
388 // -----------------------------------------------------------------------------
389 // Inspector UX: enable/disable align options depending on orientation
390 // -----------------------------------------------------------------------------
391 /**
392 * Attach inspector logic to enable/disable align options based on orientation.
393 * Uses data-inspector-key selectors produced by the Factory inspector.
394 */
395 (function attach_align_toggle() {
396
397 /**
398 * Find a <select> for a given inspector key within a root.
399 * @param {ParentNode} root
400 * @param {string} key
401 * @returns {HTMLSelectElement|null}
402 */
403 function get_sel(root, key) {
404 return /** @type {HTMLSelectElement|null} */ (root.querySelector(`select[data-inspector-key="${key}"]`));
405 }
406
407 /**
408 * Locate the inspector root (falls back to document).
409 * @returns {ParentNode}
410 */
411 function find_inspector_root() {
412 return document.querySelector('.wpbc_bfb__inspector') || document;
413 }
414
415 /**
416 * Enable/disable align options depending on orientation.
417 * If the current value becomes invalid, remap to a valid counterpart.
418 *
419 * @param {HTMLSelectElement} oriSel
420 * @param {HTMLSelectElement} alignSel
421 * @returns {void}
422 */
423 function set_disabled_options(oriSel, alignSel) {
424 if (!oriSel || !alignSel) return;
425 const ori = String(oriSel.value) === 'vertical' ? 'vertical' : 'horizontal';
426 const H = new Set(['left', 'center', 'right']);
427 const V = new Set(['top', 'middle', 'bottom']);
428
429 Array.from(alignSel.options).forEach((opt) => {
430 const v = String(opt.value);
431 const disable = (ori === 'vertical' && H.has(v)) || (ori === 'horizontal' && V.has(v));
432 opt.disabled = disable;
433 opt.hidden = disable; // hide visually as well
434 });
435
436 // If current value is disabled, remap to a valid counterpart.
437 const cur = String(alignSel.value);
438 if ((ori === 'vertical' && H.has(cur)) || (ori === 'horizontal' && V.has(cur))) {
439 const mapH = { top: 'left', middle: 'center', bottom: 'right' };
440 const mapV = { left: 'top', center: 'middle', right: 'bottom' };
441 alignSel.value = (ori === 'vertical') ? (mapV[/** @type keyof typeof mapV */(cur)] || 'middle')
442 : (mapH[/** @type keyof typeof mapH */(cur)] || 'center');
443 // Trigger change so data binding updates.
444 alignSel.dispatchEvent(new Event('change', { bubbles: true }));
445 }
446 }
447
448 /**
449 * Refresh orientation/align coupling for the current inspector.
450 * @returns {void}
451 */
452 function refresh() {
453 const root = find_inspector_root();
454 const oriSel = get_sel(root, 'orientation');
455 const alignSel = get_sel(root, 'align');
456 if (oriSel && alignSel) set_disabled_options(oriSel, alignSel);
457 }
458
459 // React to changes of the Orientation control.
460 document.addEventListener('change', (e) => {
461 const t = /** @type {Element|null} */ (e.target);
462 if (!t) return;
463 if (t.matches('select[data-inspector-key="orientation"]')) {
464 requestAnimationFrame(refresh);
465 }
466 }, true);
467
468 // Also react to inspector re-renders.
469 const mo = new MutationObserver(() => requestAnimationFrame(refresh));
470 mo.observe(document.body, { childList: true, subtree: true });
471
472 // Initial pass.
473 requestAnimationFrame(refresh);
474 })();
475
476 })(window);
477