PluginProbe
Booking Calendar / 11.2
Booking Calendar v11.2
11.9 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 All 205 releases
booking / includes / page-form-builder / field-packs / checkbox / _src / field-checkbox-wptpl.js

field-checkbox-wptpl.js in Booking Calendar 11.2, at includes/page-form-builder/field-packs/checkbox/_src/field-checkbox-wptpl.js

464 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPBC BFB: Field Renderer for "checkbox" (WP-template–driven Reference, no fallback)
3 * =========================================================================
4 * - Uses wp.template('...') for preview
5 * - Inspector UI is produced by wp.template('...')
6 * - Extends Select_Base for shared options editor wiring
7 *
8 * Contracts:
9 * - Registry: WPBC_BFB_Field_Renderer_Registry.register('checkbox', Class)
10 * - Class API: static get_defaults(), static render(el, data, ctx), static on_field_drop(data, el, ctx?) [optional]
11 *
12 * File: ../includes/page-form-builder/field-packs/checkbox/_out/field-checkbox-wptpl.js
13 *
14 * @since 11.0.0
15 * @modified 2025-09-13 11:40
16 * @version 1.0.0
17 */
18 (function (w) {
19 'use strict';
20
21 var Core = w.WPBC_BFB_Core || {};
22 var Registry = Core.WPBC_BFB_Field_Renderer_Registry;
23 var Select_Base = Core.WPBC_BFB_Select_Base;
24 // Polyfill inline helpers on the base (static) so other packs can call them safely
25 if ( !Select_Base.apply_inline_toggle_to_css ) {
26 Select_Base.apply_inline_toggle_to_css = function () { /* no-op; Checkbox overrides */
27 };
28 }
29 if ( !Select_Base.sync_inline_toggle_from_css ) {
30 Select_Base.sync_inline_toggle_from_css = function () { /* no-op; Checkbox overrides */
31 };
32 }
33
34 if ( ! Registry?.register || ! Select_Base ) {
35 w._wpbc?.dev?.error?.( 'WPBC_BFB_Field_Checkbox', 'Registry or Select_Base missing' );
36 return;
37 }
38
39 class WPBC_BFB_Field_Checkbox extends Select_Base {
40
41 static template_id = 'wpbc-bfb-field-checkbox';
42 static option_row_template_id = 'wpbc-bfb-inspector-checkbox-option-row';
43 static kind = 'checkbox';
44
45 static ui = Object.assign( {}, Select_Base.ui, {
46 cssclass_input: '.inspector__input[data-inspector-key="cssclass"]',
47 inline_toggle : '.inspector__checkbox.inspector__input[data-inspector-key="layout_inline"]'
48 } );
49
50 // ---- Inline layout helpers used by wire_once_checkbox ----
51 static get_cssclass_input(panel) {
52 return panel ? panel.querySelector( this.ui.cssclass_input ) : null;
53 }
54
55 static get_inline_toggle(panel) {
56 return panel ? panel.querySelector( this.ui.inline_toggle ) : null;
57 }
58
59 static has_inline_class(s) {
60 s = String( s || '' ).trim();
61 return s ? (' ' + s.replace( /\s+/g, ' ' ) + ' ').indexOf( ' group_inline ' ) !== -1 : false;
62 }
63
64 static add_inline_class(s) {
65 const t = String( s || '' ).trim().split( /\s+/ ).filter( Boolean );
66 if ( !t.includes( 'group_inline' ) ) t.push( 'group_inline' );
67 return t.join( ' ' ).trim();
68 }
69
70 static remove_inline_class(s) {
71 const t = String( s || '' ).trim().split( /\s+/ ).filter( Boolean );
72 return t.filter( x => x !== 'group_inline' ).join( ' ' ).trim();
73 }
74
75 static sync_inline_toggle_from_css(panel) {
76 const inp = this.get_cssclass_input( panel ), tog = this.get_inline_toggle( panel );
77 if ( !inp || !tog ) return;
78 const has = this.has_inline_class( inp.value );
79 tog.checked = !!has;
80 tog.setAttribute( 'aria-checked', has ? 'true' : 'false' );
81 }
82
83 static apply_inline_toggle_to_css(panel, on) {
84 const inp = this.get_cssclass_input( panel ), tog = this.get_inline_toggle( panel );
85 if ( !inp || !tog ) return;
86 const prev = String( inp.value || '' );
87 const next = on ? this.add_inline_class( prev ) : this.remove_inline_class( prev );
88 if ( next !== prev ) {
89 inp.value = next;
90 try {
91 inp.dispatchEvent( new Event( 'input', { bubbles: true } ) );
92 inp.dispatchEvent( new Event( 'change', { bubbles: true } ) );
93 } catch ( _ ) {
94 }
95 try {
96 const Core = window.WPBC_BFB_Core || {};
97 const field = panel.__selectbase_field
98 || document.querySelector( '.wpbc_bfb__field.is-selected, .wpbc_bfb__field--selected' );
99 if ( field ) {
100 field.dataset.cssclass = next;
101 field.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', {
102 bubbles: true, detail: { key: 'cssclass', value: next }
103 } ) );
104 Core.Structure?.update_field_prop?.( field, 'cssclass', next );
105 }
106 } catch ( _ ) {
107 }
108 }
109 tog.setAttribute( 'aria-checked', on ? 'true' : 'false' );
110 }
111
112 /* --------------------------------------------------------------------------------------------------------- */
113
114 static get_defaults() {
115 return {
116 type : 'checkbox',
117 label : 'Checkbox',
118 name : '',
119 html_id : '',
120 required : false,
121 cssclass : '',
122 help : '',
123 default_value: '',
124 value_differs: true,
125 multiple : true,
126 options : [
127 { label: 'Option 1', value: 'Option 1', selected: false },
128 { label: 'Option 2', value: 'Option 2', selected: false },
129 { label: 'Option 3', value: 'Option 3', selected: false }
130 ],
131 min_width : '240px'
132 };
133 }
134
135 /**
136 * Ensure "multiple" is set and persisted at the moment of drop.
137 * This prevents the first Inspector hydration from unchecking the hidden toggle.
138 */
139 static on_field_drop(data, el, meta) {
140 try { super.on_field_drop?.( data, el, meta ); } catch ( _ ) {}
141 if ( meta?.context !== 'drop' ) { return; }
142
143 // If not explicitly set, force it ON and persist to builder state.
144 if ( ! Object.prototype.hasOwnProperty.call( data, 'multiple' ) ) {
145 data.multiple = true;
146 }
147 try {
148 // Mirror to the canvas element & model so future hydrations read TRUE.
149 el.dataset.multiple = '1';
150 el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', {
151 bubbles: true, detail: { key: 'multiple', value: true }
152 } ) );
153 WPBC_BFB_Core.Structure?.update_field_prop?.( el, 'multiple', true );
154 } catch ( _ ) {}
155 }
156
157 /* =========================
158 * NEW: checkbox-only helpers
159 * ========================= */
160
161 /** Ensure we act only for a Checkbox inspector panel. */
162 static is_checkbox_panel( panel ) {
163 const f = panel?.__selectbase_field
164 || document.querySelector( '.wpbc_bfb__field.is-selected, .wpbc_bfb__field--selected' );
165 const t = (f?.dataset?.type || f?.getAttribute?.('data-type') || '').toLowerCase();
166 return t === 'checkbox';
167 }
168
169 /** Convert any fallback radio icons → checkbox icons inside the options list. */
170 static fix_option_toggle_icons( panel ) {
171 if ( ! panel || ! this.is_checkbox_panel( panel ) ) { return; }
172 const list = panel.querySelector( this.ui.list );
173 if ( ! list ) { return; }
174 const wrong = list.querySelectorAll( '.wpbc_ui__toggle_icon_radio' );
175 for ( var i = 0; i < wrong.length; i++ ) {
176 wrong[i].classList.remove( 'wpbc_ui__toggle_icon_radio' );
177 wrong[i].classList.add( 'wpbc_ui__toggle_icon_checkbox' );
178 }
179 }
180
181 /**
182 * Force “multiple mode” for the Select_Base logic (so defaults are NOT exclusive).
183 * We add a hidden, checked control that Select_Base checks via .js-opt-multiple[data-inspector-key="multiple"].
184 */
185 static ensure_multiple_mode_marker( panel ) {
186 if ( ! panel || ! this.is_checkbox_panel( panel ) ) { return; }
187
188 let mult = panel.querySelector( '.js-opt-multiple[data-inspector-key="multiple"]' );
189 if ( ! mult ) {
190 mult = document.createElement( 'input' );
191 mult.type = 'checkbox';
192 mult.className = 'js-opt-multiple inspector__input';
193 mult.setAttribute( 'data-inspector-key', 'multiple' );
194 mult.checked = true;
195 mult.hidden = true;
196 // hide from layout & a11y tree, but keep it discoverable by querySelector
197 mult.style.display = 'none';
198 mult.setAttribute( 'aria-hidden', 'true' );
199 mult.setAttribute( 'tabindex', '-1' );
200
201 const group = panel.querySelector( '.wpbc_bfb__inspector__group[data-group="options"] .group__fields' )
202 || panel.querySelector( '.wpbc_bfb__inspector__body' ) || panel;
203
204 group.appendChild( mult );
205 } else if ( ! mult.checked ) {
206 mult.checked = true;
207 }
208 }
209
210 /**
211 * Observe the options list so any rows appended by the parent (fallback path)
212 * are auto-corrected to use checkbox icons.
213 */
214 static observe_options_list( panel ) {
215 if ( ! panel || panel.__wpbc_cb_obs ) { return; }
216 const list = panel.querySelector( this.ui.list );
217 if ( ! list ) { return; }
218
219 const obs = new MutationObserver( (mutations) => {
220 for ( var i = 0; i < mutations.length; i++ ) {
221 if ( mutations[i].type !== 'childList' || !mutations[i].addedNodes?.length ) { continue; }
222 // Any new rows → fix icons immediately.
223 this.fix_option_toggle_icons( panel );
224 }
225 } );
226
227 try {
228 obs.observe( list, { childList: true } );
229 panel.__wpbc_cb_obs = obs;
230 } catch ( _ ) {}
231 }
232
233 static wire_once_checkbox() {
234 if ( this.__checkbox_wired ) { return; }
235 this.__checkbox_wired = true;
236
237 const on_ready_or_render = ( ev ) => {
238 const panel = ev?.detail?.panel;
239 if ( ! panel ) { return; }
240
241 // Let Select_Base wire itself.
242 this.bootstrap_panel( panel );
243
244 // 1) Multiple defaults ON for Checkbox (prevents exclusivity).
245 this.ensure_multiple_mode_marker( panel );
246
247 // 2) Correct any existing fallback icons (initial render).
248 this.fix_option_toggle_icons( panel );
249
250 // 3) Keep fixing icons for any newly added/duplicated rows.
251 this.observe_options_list( panel );
252
253 // Reflect cssclass → inline toggle (existing behavior).
254 this.sync_inline_toggle_from_css( panel );
255 };
256
257 document.addEventListener( 'wpbc_bfb_inspector_ready', on_ready_or_render );
258 document.addEventListener( 'wpbc_bfb_inspector_render', on_ready_or_render );
259
260 const root = document.getElementById( 'wpbc_bfb__inspector' );
261 if ( ! root ) { return; }
262
263 const get_panel = ( t ) => t?.closest?.( '.wpbc_bfb__inspector__body' ) || root.querySelector( '.wpbc_bfb__inspector__body' ) || null;
264
265 // Keep multiple-mode and icons correct AFTER parent handles clicks (add/duplicate)
266 // NOTE: parent stops propagation in its click handler; we schedule our fix in CAPTURE,
267 // then run it async (setTimeout 0) so DOM is already updated.
268 root.addEventListener( 'click', ( e ) => {
269 const panel = get_panel( e.target );
270 if ( ! panel || ! this.is_checkbox_panel( panel ) ) { return; }
271 const is_add_btn = e.target.closest?.( this.ui.add_btn );
272 const is_menu_action = e.target.closest?.( this.ui.menu_action );
273 if ( is_add_btn || is_menu_action ) {
274 setTimeout( () => {
275 this.ensure_multiple_mode_marker( panel ); // ensures non-exclusive
276 this.fix_option_toggle_icons( panel ); // fixes icon class
277 }, 0 );
278 }
279 }, true ); // CAPTURE phase
280
281 // Inline layout toggle & manual cssclass edits (unchanged)
282 root.addEventListener( 'change', ( e ) => {
283 const panel = get_panel( e.target );
284 if ( ! panel || ! this.is_checkbox_panel( panel ) ) { return; }
285 if ( e.target.matches?.( this.ui.inline_toggle ) ) {
286 e.target.setAttribute( 'aria-checked', e.target.checked ? 'true' : 'false' );
287 this.apply_inline_toggle_to_css( panel, !! e.target.checked );
288 }
289 }, true );
290
291 root.addEventListener( 'input', ( e ) => {
292 const panel = get_panel( e.target );
293 if ( ! panel || ! this.is_checkbox_panel( panel ) ) { return; }
294 if ( e.target.matches?.( this.ui.cssclass_input ) ) {
295 this.sync_inline_toggle_from_css( panel );
296 }
297 }, true );
298
299 // Optional: bootstrap immediately if inspector is already on screen
300 setTimeout( () => {
301 const panel = document.querySelector( '#wpbc_bfb__inspector .wpbc_bfb__inspector__body' );
302 if ( panel && this.is_checkbox_panel( panel ) ) {
303 this.bootstrap_panel( panel );
304 this.ensure_multiple_mode_marker( panel );
305 this.fix_option_toggle_icons( panel );
306 this.observe_options_list( panel );
307 this.sync_inline_toggle_from_css( panel );
308 }
309 }, 0 );
310 }
311 }
312
313 try { Registry.register( 'checkbox', WPBC_BFB_Field_Checkbox ); } catch ( e ) { w._wpbc?.dev?.error?.( 'WPBC_BFB_Field_Checkbox.register', e ); }
314 w.WPBC_BFB_Field_Checkbox = WPBC_BFB_Field_Checkbox;
315 try { WPBC_BFB_Field_Checkbox.wire_once_checkbox(); } catch ( _ ) {}
316
317
318 // -----------------------------------------------------------------------------------------------------------------
319 // Export for "Booking Form" (Advanced Form shortcode)
320 // -----------------------------------------------------------------------------------------------------------------
321 /**
322 * Booking Form exporter callback (Advanced Form shortcode) for "checkbox".
323 *
324 * This callback is registered per field type via:
325 * WPBC_BFB_Exporter.register( 'checkbox', callback )
326 *
327 * Core call site (builder-exporter.js):
328 * WPBC_BFB_Exporter.run_registered_exporter( field, io, cfg, once, ctx )
329 * → callback( field, emit, { io, cfg, once, ctx, core } );
330 *
331 * @callback WPBC_BFB_ExporterCallback
332 * @param {Object} field
333 * Normalized field data coming from the Builder structure.
334 * - field.type {string} Field type, here "checkbox".
335 * - field.label {string} Visible label in the form (may be empty).
336 * - field.name {string} Name as stored on the canvas (already validated).
337 * - field.html_id {string} Optional HTML id / user-visible id.
338 * - field.cssclass {string} Extra CSS classes entered in Inspector.
339 * - field.options {Array} Options for checkbox group (label/value/selected).
340 * - field.option_label {string} Optional single-checkbox label (used when no options[]).
341 * - field.placeholder {string} Optional placeholder (used as fallback label).
342 * - ... (Any other pack-specific props are also present.)
343 *
344 * @param {function(string):void} emit
345 * Emits one line/fragment into the export buffer.
346 *
347 * @param {Object} [extras]
348 * Extra context passed by the core exporter.
349 */
350 function register_checkbox_booking_form_exporter() {
351
352 const Exp = w.WPBC_BFB_Exporter;
353 if ( ! Exp || typeof Exp.register !== 'function' ) { return; }
354 if ( typeof Exp.has_exporter === 'function' && Exp.has_exporter( 'checkbox' ) ) { return; }
355
356 const S = Core.WPBC_BFB_Sanitize || (w.WPBC_BFB_Core && w.WPBC_BFB_Core.WPBC_BFB_Sanitize) || {};
357 const esc_html = S.escape_html || (v => String( v ));
358 const esc_sc = S.escape_for_shortcode || (v => String( v ));
359
360 /**
361 * @type {WPBC_BFB_ExporterCallback}
362 */
363 Exp.register( 'checkbox', (field, emit, extras = {}) => {
364
365 const cfg = extras.cfg || {};
366 const ctx = extras.ctx;
367 const addLabels = cfg.addLabels !== false;
368
369 // Required marker (same semantics as other text-like fields).
370 const is_req = Exp.is_required( field );
371 const req_mark = is_req ? '*' : '';
372
373 // Name / id / classes come from shared helpers so they stay in sync with other packs.
374 const name = Exp.compute_name( 'checkbox', field );
375 const id_opt = Exp.id_option( field, ctx );
376 const cls_opts = Exp.class_options( field );
377
378 // Options + default (same helpers as legacy path).
379 const tokens = Exp.option_tokens( field );
380 const def = Exp.default_option_suffix( field, tokens );
381
382 // Always add bare `use_label_element` (no value/quotes) for checkbox shortcodes.
383 const use_label_element_token = ' use_label_element';
384
385 const raw_label = (field && typeof field.label === 'string') ? field.label : '';
386 const label = raw_label.trim();
387
388 let body;
389
390 if ( ! tokens.trim() ) {
391 // Single checkbox (no options array) — keep label in quotes at the end.
392 const single_label = field.option_label || field.placeholder || label || 'I agree';
393
394 // ORDER: name id cls use_label_element "label".
395 body = `[checkbox${req_mark} ${name}${id_opt}${cls_opts}${use_label_element_token} "${esc_sc( single_label )}"]`;
396 } else {
397 // Multiple checkboxes (options array) — keep default BEFORE tokens.
398 // ORDER: name id cls use_label_element default tokens
399 body = `[checkbox${req_mark} ${name}${id_opt}${cls_opts}${use_label_element_token}${def}${tokens}]`;
400 }
401
402 // Label behavior mirrors legacy emit_checkbox_singles().
403 if ( label && addLabels ) {
404 emit( `<l>${esc_html( label )}${req_mark}</l>` );
405 emit( `<br>${body}` );
406 } else {
407 emit( body );
408 }
409 // Help text is appended centrally by WPBC_BFB_Exporter.render_field_node().
410 } );
411 }
412
413 // Try immediate registration; if core isn’t ready, wait for the event.
414 if ( w.WPBC_BFB_Exporter && typeof w.WPBC_BFB_Exporter.register === 'function' ) {
415 register_checkbox_booking_form_exporter();
416 } else {
417 document.addEventListener( 'wpbc:bfb:exporter-ready', register_checkbox_booking_form_exporter, { once: true } );
418 }
419
420
421 // -----------------------------------------------------------------------------------------------------------------
422 // Export for "Booking Data" (Content of booking fields data)
423 // -----------------------------------------------------------------------------------------------------------------
424 /**
425 * Booking Data exporter callback ("Content of booking fields data") for "checkbox".
426 *
427 * Default behavior:
428 * <b>Label</b>: <f>[field_name]</f><br>
429 *
430 * Registered per field type via:
431 * WPBC_BFB_ContentExporter.register( 'checkbox', callback )
432 */
433 function register_checkbox_booking_data_exporter() {
434
435 const C = w.WPBC_BFB_ContentExporter;
436 if ( ! C || typeof C.register !== 'function' ) { return; }
437 if ( typeof C.has_exporter === 'function' && C.has_exporter( 'checkbox' ) ) { return; }
438
439 C.register( 'checkbox', function (field, emit, extras) {
440
441 extras = extras || {};
442 const cfg = extras.cfg || {};
443
444 const Exp = w.WPBC_BFB_Exporter;
445 if ( ! Exp || typeof Exp.compute_name !== 'function' ) { return; }
446
447 const name = Exp.compute_name( 'checkbox', field );
448 if ( ! name ) { return; }
449
450 const label = (typeof field.label === 'string' && field.label.trim()) ? field.label.trim() : name;
451
452 // Shared formatter keeps all packs consistent.
453 C.emit_line_bold_field( emit, label, name, cfg );
454 } );
455 }
456
457 if ( w.WPBC_BFB_ContentExporter && typeof w.WPBC_BFB_ContentExporter.register === 'function' ) {
458 register_checkbox_booking_data_exporter();
459 } else {
460 document.addEventListener( 'wpbc:bfb:content-exporter-ready', register_checkbox_booking_data_exporter, { once: true } );
461 }
462
463 })( window );
464