PluginProbe
Booking Calendar / 11.5
Booking Calendar v11.5
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 / section / _src / ui-column-styles.js

ui-column-styles.js in Booking Calendar 11.5, at includes/page-form-builder/field-packs/section/_src/ui-column-styles.js

800 lines 30.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * == How to add a new CSS style ? ==
3 *
4 * TL;DR: add a key in COL_PROPS (JS) -> reference its CSS var (CSS) → add a control in the inspector template (HTML).
5 * Keep the JS default (def) and the CSS var() fallback identical.
6 *
7 * -- 1) Register the style in this JS (COL_PROPS)
8 *
9 * // Length-like example:
10 * pad: { var: '--wpbc-bfb-col-pad', def: '0px', normalize: 'len' }
11 *
12 * // Enum example:
13 * ac: { var: '--wpbc-bfb-col-ac', def: 'normal', normalize: { type: 'enum', values: ['normal','stretch','center','start','end','space-between','space-around','space-evenly'] } }
14 *
15 * Notes:
16 * - Allowed normalizers: 'id' (passthrough), 'len' (px/rem/em/%), or {type:'enum',values:[...] }.
17 * - If you need a new normalizer, add it to NORMALIZE and reference its name here.
18 *
19 * -- 2) Wire the CSS variable (defaults + activation)
20 *
21 * File with defaults: ../includes/__css/client/form_fields/bfb_section__columns.css
22 * The fallback in var(--name, <fallback>) MUST MATCH COL_PROPS[key].def.
23 *
24 * /* Mini preview (template “ghost” columns; always on) *\/
25 * .wpbc_bfb__section__cols > .wpbc_bfb__section__col {
26 * padding: var(--wpbc-bfb-col-pad, 0px);
27 * /* add other properties here using their vars *\/
28 * }
29 *
30 * /* Real columns (only when styles are activated) *\/
31 * .wpbc_bfb_form .wpbc_bfb__section[data-colstyles-active="1"] > .wpbc_bfb__row > .wpbc_bfb__column {
32 * padding: var(--wpbc-bfb-col-pad, 0px);
33 * }
34 *
35 * Where “default CSS settings” live:
36 * - The JS default: COL_PROPS[key].def (in this file) — used for parsing, preview, and activation checks.
37 * - The CSS fallback: var(--wpbc-bfb-col-<key>, <fallback>) — in bfb_section__columns.css, must equal the JS default.
38 *
39 * -- 3) Add an inspector control in the template (tmpl-wpbc-bfb-column-styles)
40 * File: ../includes/page-form-builder/field-packs/section/section-wptpl.php
41 *
42 * <!-- Simple input (works for 'len', 'id', and many enums with text inputs) -->
43 * <div class="inspector__row">
44 * <label class="inspector__label inspector__w_40">Padding</label>
45 * <div class="inspector__control inspector__w_50">
46 * <input type="text" class="inspector__input" data-style-key="pad" data-col-idx="{{ i }}" placeholder="e.g., 8px or 0.5rem">
47 * </div>
48 * </div>
49 *
50 * The generic change handler will:
51 * - read data-style-key,
52 * - normalize via COL_PROPS,
53 * - persist to data-col_styles (sparse JSON; defaults stripped),
54 * - toggle data-colstyles-active automatically,
55 * - and update preview + real columns.
56 *
57 * -- 4) (Optional) Split value styles (like GAP number+unit)
58 *
59 * If your new style is a pair (value + unit), mirror the 'gap' pattern:
60 * - two inputs with data-style-part="value" and data-style-part="unit"
61 * - add a small branch in on_change (like the existing one for key === 'gap') that combines value+unit
62 * before normalizing and saving.
63 *
64 * -- 5) Activation & persistence (what happens under the hood)
65 *
66 * - The service compares saved values vs COL_PROPS defaults. If any non-default exists, it sets
67 * data-colstyles-active="1" on the section and writes CSS vars to real columns.
68 * - When inactive, the service removes inline vars so CSS falls back to your default in var(..., fallback).
69 * - data-col_styles attribute stores a compact (sparse) JSON: only keys that differ from defaults are saved.
70 *
71 * Checklist before you ship:
72 * [ ] COL_PROPS entry added with correct var name and def value
73 * [ ] CSS var() fallback matches the JS default exactly
74 * [ ] Inspector control present and uses data-style-key="<your key>"
75 * [ ] (If split-value) on_change branch added, like for 'gap'
76 */
77
78
79 /**
80 * UI: Column Styles (service + inspector component)
81 * ---------------------------------------------------------------------------------
82 * Splits column-style logic out of Section renderer:
83 * - Service: UI.WPBC_BFB_Column_Styles (parse/stringify/apply/is_active/baseline)
84 * - Inspector slot: UI.wpbc_bfb_column_styles (render_for_section / refresh_for_section)
85 *
86 * == File: /includes/page-form-builder/field-packs/section/_out/ui-column-styles.js
87 *
88 * @since 11.0.0
89 * @modified 2025-09-16 11:25
90 * @version 1.0.0
91 */
92 (function ( w ) {
93 'use strict';
94
95 var Core = ( w.WPBC_BFB_Core = w.WPBC_BFB_Core || {} );
96 var UI = ( Core.UI = Core.UI || {} );
97
98 var S = Core.WPBC_BFB_Sanitize || {};
99 var DOM = ( Core.WPBC_BFB_DOM && Core.WPBC_BFB_DOM.SELECTORS ) || {
100 row : '.wpbc_bfb__row',
101 column : '.wpbc_bfb__column'
102 };
103
104 // -----------------------------------------------------------------------------------------------------------------
105 /**
106 * Central registry of supported per-column CSS properties.
107 *
108 * Each entry describes how a logical style key maps to a CSS custom property
109 * written on the column element, its default value, and how to normalize user
110 * input before persisting/applying.
111 *
112 * key — Short style key used in UI and persisted JSON.
113 * var — CSS variable name written onto the DOM node style.
114 * def — Default value if the style is unset/empty.
115 * normalize — Normalizer id ('id' passthrough, 'len' for length units).
116 */
117 var COL_PROPS = {
118 dir : { var: '--wpbc-bfb-col-dir', def: 'column', normalize: 'id' },
119 wrap : { var: '--wpbc-bfb-col-wrap', def: 'nowrap', normalize: 'id' },
120 jc : { var: '--wpbc-bfb-col-jc', def: 'flex-start', normalize: 'id' },
121 ai : { var: '--wpbc-bfb-col-ai', def: 'stretch', normalize: 'id' },
122 gap : { var: '--wpbc-bfb-col-gap', def: '0px', normalize: 'len' },
123 aself: { var: '--wpbc-bfb-col-aself', def: 'flex-start',
124 normalize: { type: 'enum', values: [ 'flex-start', 'center', 'flex-end', 'stretch' ] }
125 }
126 // Example additions:
127 // pad : { var: '--wpbc-bfb-col-pad', def: '0px', normalize: 'len' }
128 };
129
130 /**
131 * Normalize a "length-like" value (e.g., "8" → "8px").
132 * Accepts px/rem/em/%; expand the regex if you allow more units.
133 *
134 * @param {string|number} v
135 * @returns {string} normalized value (always non-empty)
136 */
137 function norm_len( v ) {
138 var sv = String( v || '' ).trim();
139 if ( ! sv ) { return '0px'; }
140 if ( /^\d+(\.\d+)?$/.test( sv ) ) { return sv + 'px'; } // number -> px.
141 if ( /^\d+(\.\d+)?(px|rem|em|%)$/.test( sv ) ) { return sv; } // allowed units.
142 return '0px';
143 }
144
145 /**
146 * Example: ac: { var: '--wpbc-bfb-col-ac', def: 'normal', normalize: { type: 'enum', values: ['normal','stretch','center','start','end','space-between','space-around','space-evenly'] } }
147 *
148 * @param v
149 * @param vals
150 * @returns {string|*}
151 */
152 function norm_enum(v, vals) {
153 v = String( v || '' );
154 return vals.indexOf( v ) !== -1 ? v : vals[0];
155 }
156
157 /**
158 * Normalizer registry. Extend to add custom validators/transforms
159 * (e.g., enums, numbers with ranges, etc.).
160 */
161 var NORMALIZE = {
162 id : v => String( v || '' ),
163 len : norm_len,
164 enum: (v, values) => norm_enum( v, values )
165 };
166
167 /**
168 * Check whether a style key is supported by COL_PROPS.
169 *
170 * @param {string} k
171 * @returns {boolean}
172 */
173 function is_supported_key( k ) {
174 return Object.prototype.hasOwnProperty.call( COL_PROPS, k );
175 }
176
177 /**
178 * Normalize a value for a given style key using its configured normalizer.
179 *
180 * @param {string} key
181 * @param {any} val
182 * @returns {string}
183 */
184 function normalize_value(key, val) {
185 var cfg = COL_PROPS[key];
186 if ( ! cfg ) {
187 return String( val || '' );
188 }
189 if ( cfg.normalize && typeof cfg.normalize === 'object' && cfg.normalize.type === 'enum' ) {
190 return NORMALIZE.enum( val, cfg.normalize.values || [] );
191 }
192 var fn = NORMALIZE[cfg.normalize] || NORMALIZE.id;
193 return fn( val );
194 }
195
196 /**
197 * Build a plain object containing defaults for all supported style keys.
198 *
199 * @returns {Record<string, string>}
200 */
201 function get_defaults_obj() {
202 var o = {}; for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) { o[k] = COL_PROPS[k].def; } }
203 return o;
204 }
205
206 /**
207 * Apply all CSS variables (from COL_PROPS) onto a node based on a style object.
208 * Missing/empty values fall back to defaults.
209 *
210 * @param {HTMLElement} node
211 * @param {Record<string, string>} style_obj
212 */
213 function set_vars( node, style_obj ) {
214 if ( ! node ) { return; }
215 for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) {
216 var cssVar = COL_PROPS[k].var;
217 var v = ( style_obj && style_obj[k] != null && String( style_obj[k] ).trim() !== '' ) ? style_obj[k] : COL_PROPS[k].def;
218 node.style.setProperty( cssVar, String( v ) );
219 }}
220 }
221
222 function set_vars_sparse(node, style_obj) {
223 if ( !node ) return;
224 for ( var k in COL_PROPS ) { if ( is_supported_key(k) ) {
225 var cssVar = COL_PROPS[k].var;
226 if ( style_obj && Object.prototype.hasOwnProperty.call(style_obj, k) && String(style_obj[k]).trim() !== '' ) {
227 node.style.setProperty(cssVar, String(style_obj[k]));
228 } else {
229 // important: remove var instead of writing a default
230 node.style.removeProperty(cssVar);
231 }
232 }}
233 }
234
235 /**
236 * Remove all CSS variables (from COL_PROPS) from a node.
237 *
238 * @param {HTMLElement} node
239 */
240 function clear_vars( node ) {
241 if ( ! node ) { return; }
242 for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) {
243 node.style.removeProperty( COL_PROPS[k].var );
244 }}
245 }
246
247 /**
248 * Clamp helper for columns number.
249 *
250 * @param {number|string} n
251 * @returns {number}
252 */
253 function clamp_cols( n ) {
254 return ( S.clamp ? S.clamp( Number( n ) || 1, 1, 4 ) : Math.max( 1, Math.min( 4, Number( n ) || 1 ) ) );
255 }
256
257 /**
258 * Read actual number of columns from DOM.
259 *
260 * @param {HTMLElement} el
261 * @returns {number}
262 */
263 function dom_cols( el ) {
264 try {
265 var row = el ? el.querySelector( ':scope > ' + DOM.row ) : null;
266 var cnt = row ? row.querySelectorAll( ':scope > ' + DOM.column ).length : 1;
267 return clamp_cols( cnt );
268 } catch ( _e ) {
269 return 1;
270 }
271 }
272
273 // ------------------------------------------------------------------------------------------------
274 // Service: Column Styles
275 // ------------------------------------------------------------------------------------------------
276 UI.WPBC_BFB_Column_Styles = {
277
278 /**
279 * Highlight a specific column in both the live canvas and the mini preview,
280 * and store the active index on the section.
281 *
282 * @param {HTMLElement} section_el
283 * @param {number|string} key_1based 1-based column index (clamped)
284 */
285 set_selected_col_flag : function ( section_el, key_1based ) {
286 if ( ! section_el ) { return; }
287 var cols_cnt = dom_cols( section_el );
288 var idx = Math.min( Math.max( parseInt( key_1based, 10 ) || 1, 1 ), cols_cnt || 1 );
289 var idx0 = idx - 1;
290
291 section_el.setAttribute( 'data-selected-col', String( idx ) );
292
293 // Real canvas columns.
294 var row = section_el.querySelector( ':scope > ' + DOM.row );
295 var cols = row ? row.querySelectorAll( ':scope > ' + DOM.column ) : [];
296 for ( var i = 0; i < cols.length; i++ ) {
297 if ( cols[i].classList ) {
298 cols[i].classList.toggle( 'is-selected-column', i === idx0 );
299 }
300 }
301
302 // Mini preview columns.
303 var pcols = section_el.querySelectorAll( ':scope .wpbc_bfb__section__cols > .wpbc_bfb__section__col' );
304 for ( var j = 0; j < pcols.length; j++ ) {
305 if ( pcols[j].classList ) {
306 pcols[j].classList.toggle( 'is-selected-column', j === idx0 );
307 }
308 }
309 },
310
311 /**
312 * Remove column selection highlight from both canvas and mini preview.
313 *
314 * @param {HTMLElement} section_el
315 */
316 clear_selected_col_flag : function ( section_el ) {
317 if ( ! section_el ) { return; }
318 section_el.removeAttribute( 'data-selected-col' );
319
320 var row = section_el.querySelector( ':scope > ' + DOM.row );
321 var cols = row ? row.querySelectorAll( ':scope > ' + DOM.column ) : [];
322 for ( var i = 0; i < cols.length; i++ ) {
323 cols[i].classList && cols[i].classList.remove( 'is-selected-column' );
324 }
325
326 var pcols = section_el.querySelectorAll( ':scope .wpbc_bfb__section__cols > .wpbc_bfb__section__col' );
327 for ( var j = 0; j < pcols.length; j++ ) {
328 pcols[j].classList && pcols[j].classList.remove( 'is-selected-column' );
329 }
330 },
331
332
333
334 /**
335 * Parse JSON string to array of per-column style objects.
336 *
337 * @param {string} s
338 * @returns {Array<{dir:string,wrap:string,jc:string,ai:string,gap:string}>}
339 */
340 parse_col_styles : function ( s ) {
341 if ( ! s ) { return []; }
342 var obj = ( S.safe_json_parse ? S.safe_json_parse( String( s ), null ) : ( function(){ try { return JSON.parse( String( s ) ); } catch( _e ){ return null; } } )() );
343 if ( Array.isArray( obj ) ) { return obj; }
344 if ( obj && typeof obj === 'object' && Array.isArray( obj.columns ) ) { return obj.columns; }
345 return [];
346 },
347
348 /**
349 * Stringify styles array to canonical JSON.
350 *
351 * @param {Array} arr
352 * @returns {string}
353 */
354 stringify_col_styles : function ( arr ) {
355 var data = Array.isArray( arr ) ? arr : [];
356 return ( S.stringify_data_value ? S.stringify_data_value( data ) : JSON.stringify( data ) );
357 },
358
359 /**
360 * Check if per-column styles are active for a section.
361 * - Active if element flag data-colstyles-active="1" OR non-empty serialized styles.
362 *
363 * @param {HTMLElement} section_el
364 * @returns {boolean}
365 */
366 is_active : function ( section_el ) {
367 if ( ! section_el ) { return false; }
368 if ( section_el.getAttribute( 'data-colstyles-active' ) === '1' ) { return true; }
369 var raw = section_el.getAttribute( 'data-col_styles' ) || ( section_el.dataset ? ( section_el.dataset.col_styles || '' ) : '' );
370 var arr = this.parse_col_styles( raw );
371 var DEF = get_defaults_obj();
372 // Active only if any column has a non-default, non-empty override.
373 for ( var i = 0; i < arr.length; i++ ) {
374 var s = arr[i] || {};
375 for ( var k in DEF ) {
376 if ( Object.prototype.hasOwnProperty.call( s, k ) ) {
377 var v = String( s[k] );
378 if ( v && v !== String( DEF[k] ) ) { return true; }
379 }
380 }
381 }
382 return false;
383 },
384
385 /**
386 * Apply per-column styles to preview and, when active, to real columns.
387 *
388 * @param {HTMLElement} section_el
389 * @param {Array} styles
390 */
391 apply : function ( section_el, styles ) {
392 if ( ! section_el ) { return; }
393
394 // Mini preview inside the section template (always on).
395 var preview = section_el.querySelector( ':scope .wpbc_bfb__section__cols' );
396 if ( preview ) {
397 var pcols = preview.querySelectorAll( ':scope > .wpbc_bfb__section__col' );
398 for ( var i = 0; i < pcols.length; i++ ) {
399 set_vars(pcols[i], styles[i] || {}); // OK to use defaults in preview
400 }
401 }
402
403 // Determine activation from current element state (not from styles arg alone).
404 var active = this.is_active( section_el );
405
406 // If not active, clean up inline vars and remove the flag.
407 if ( ! active ) {
408 section_el.removeAttribute( 'data-colstyles-active' );
409 var row_off = section_el.querySelector( ':scope > ' + DOM.row );
410 if ( row_off ) {
411 var nodes = row_off.querySelectorAll( ':scope > ' + DOM.column );
412 for ( var j = 0; j < nodes.length; j++ ) {
413 clear_vars( nodes[j] );
414 }
415 }
416 return;
417 }
418
419 // Active: add flag if missing and write CSS vars to REAL canvas columns.
420 section_el.setAttribute( 'data-colstyles-active', '1' );
421
422 // NEW: always use the sparse, saved JSON for REAL columns.
423 var use_styles = this.parse_col_styles(
424 section_el.getAttribute( 'data-col_styles' ) ||
425 (section_el.dataset ? (section_el.dataset.col_styles || '') : '')
426 );
427
428 var row = section_el.querySelector( ':scope > ' + DOM.row );
429 if ( row ) {
430 var rcols = row.querySelectorAll( ':scope > ' + DOM.column );
431 for ( var k = 0; k < rcols.length; k++ ) {
432 set_vars_sparse(rcols[k], use_styles[k] || {}); // only write what exists
433 }
434 }
435 }
436 };
437
438 // ------------------------------------------------------------------------------------------------
439 // Inspector component (slot: "column_styles")
440 // ------------------------------------------------------------------------------------------------
441 UI.wpbc_bfb_column_styles = {
442
443 /**
444 * Render the per-column style editor (wp.template: 'wpbc-bfb-column-styles').
445 *
446 * @param {object} builder
447 * @param {HTMLElement} section_el
448 * @param {HTMLElement} host
449 */
450 render_for_section : function ( builder, section_el, host ) {
451 if ( ! host || ! section_el ) { return; }
452
453 // Capture current active tab BEFORE we clear host.
454 var __prev_root = host.querySelector( '[data-wpbc-tabs]' );
455 var ds = section_el.dataset || {};
456 var __prev_key = (__prev_root && __prev_root.getAttribute( 'data-wpbc-tab-active' )) || host.__wpbc_active_key || ds.col_styles_active_tab || null;
457
458 // Cleanup previous mount and clear.
459 if ( host.__wpbc_cleanup ) {
460 try { host.__wpbc_cleanup(); } catch ( _e ) {}
461 host.__wpbc_cleanup = null;
462 }
463 host.innerHTML = '';
464
465 var tpl = ( w.wp && w.wp.template ) ? w.wp.template( 'wpbc-bfb-column-styles' ) : null;
466 if ( ! tpl ) { return; }
467
468 var col_count = dom_cols( section_el );
469 var raw_json = section_el.getAttribute( 'data-col_styles' ) || ( section_el.dataset ? ( section_el.dataset.col_styles || '' ) : '' );
470
471 var saved_arr = UI.WPBC_BFB_Column_Styles.parse_col_styles( raw_json );
472 var styles_arr = [];
473
474 // Normalize length to current columns (UI-only defaults do NOT auto-activate).
475 var def = get_defaults_obj();
476 for ( var i = 0; i < col_count; i++ ) {
477 var src = saved_arr[i] || {};
478 // Merge for display, but track which keys were actually present in saved JSON.
479 var full = Object.assign( {}, def, src );
480 full.__has = {
481 dir : Object.prototype.hasOwnProperty.call( src, 'dir' ),
482 wrap : Object.prototype.hasOwnProperty.call( src, 'wrap' ),
483 jc : Object.prototype.hasOwnProperty.call( src, 'jc' ),
484 ai : Object.prototype.hasOwnProperty.call( src, 'ai' ),
485 gap : Object.prototype.hasOwnProperty.call( src, 'gap' ),
486 aself : Object.prototype.hasOwnProperty.call( src, 'aself' )
487 };
488 styles_arr[i] = full;
489 }
490 styles_arr.length = col_count; // clamp.
491
492 host.innerHTML = tpl( {
493 cols : col_count,
494 styles: styles_arr,
495 active: UI.WPBC_BFB_Column_Styles.is_active( section_el )
496 } );
497
498 if ( window.wpbc_ui_tabs && host ) {
499 window.wpbc_ui_tabs.init_on( host );
500
501
502 // Persist the active tab so we can restore it after re-renders.
503 var tabsRoot = host.querySelector( '[data-wpbc-tabs]' );
504 if ( tabsRoot && !tabsRoot.__wpbc_persist_listener ) {
505 tabsRoot.__wpbc_persist_listener = true;
506 tabsRoot.addEventListener( 'wpbc:tabs:change', function (e) {
507 var k = e && e.detail && e.detail.active_key;
508 if ( k ) {
509 host.__wpbc_active_key = String( k );
510 if ( section_el && section_el.dataset ) {
511 section_el.dataset.col_styles_active_tab = String( k );
512 }
513 // NEW: reflect selection on the section + columns.
514 UI.WPBC_BFB_Column_Styles.set_selected_col_flag( section_el, k );
515 }
516 }, true );
517
518 }
519
520 // Restore previous tab if it still exists (clamp to new count).
521 var __key
522 if ( __prev_key ) {
523 var __new_root = host.querySelector( '[data-wpbc-tabs]' );
524 __key = String( Math.min( Math.max( parseInt( __prev_key, 10 ) || 1, 1 ), col_count ) );
525 if ( __new_root && window.wpbc_ui_tabs.set_active ) {
526 window.wpbc_ui_tabs.set_active( __new_root, __key );
527 }
528 }
529
530 // After restoring the tab, ensure highlight matches the active tab.
531 var __active_key_now = __key || (ds.col_styles_active_tab ? String( Math.min( Math.max( parseInt( ds.col_styles_active_tab, 10 ) || 1, 1 ), col_count ) ) : '1');
532 UI.WPBC_BFB_Column_Styles.set_selected_col_flag( section_el, __active_key_now );
533 }
534
535 // Re-wire number - range pairing (ValueSlider) for freshly rendered controls.
536 try {
537 UI.InspectorEnhancers && UI.InspectorEnhancers.scan && UI.InspectorEnhancers.scan( host );
538 // Alternatively (direct wiring):
539 // UI.WPBC_BFB_ValueSlider && UI.WPBC_BFB_ValueSlider.init_on && UI.WPBC_BFB_ValueSlider.init_on( host );
540 } catch ( _e ) {}
541
542 // Set initial state of ICONS (including defaults) is correct.
543 sync_axis_rotation_all();
544
545 function styles_has_any_non_default(styles_arr, get_defaults_obj_fn) {
546 var def = get_defaults_obj_fn();
547 for ( var i = 0; i < styles_arr.length; i++ ) {
548 var s = styles_arr[i] || {};
549 for ( var k in def ) {
550 if ( Object.prototype.hasOwnProperty.call( def, k ) ) {
551 var v = (s[k] == null) ? '' : String( s[k] );
552 // treat empty as "not selected" (not active).
553 if ( v && v !== String( def[k] ) ) {
554 return true;
555 }
556 }
557 }
558 }
559 return false;
560 }
561
562 function strip_defaults_for_save(styles_arr, get_defaults_obj_fn) {
563 var def = get_defaults_obj_fn();
564 var out = [];
565 for ( var i = 0; i < styles_arr.length; i++ ) {
566 var s = styles_arr[i] || {};
567 var row = {};
568 for ( var k in def ) {
569 if ( Object.prototype.hasOwnProperty.call( def, k ) ) {
570 var v = (s[k] == null) ? '' : String( s[k] );
571 if ( v && v !== String( def[k] ) ) {
572 row[k] = v; // only keep meaningful overrides.
573 }
574 }
575 }
576 out.push( row );
577 }
578 return out;
579 }
580
581 /**
582 * Toggle rotation class for the chip labels of a specific column and group set.
583 *
584 * @param {number} idx Column index (0-based)
585 * @param {boolean} enable Whether to add (true) or remove (false) the rotation class
586 */
587 function toggle_axis_rotation_for_col( idx, enable ) {
588 var keys = [ 'ai', 'jc' ];
589 for ( var g = 0; g < keys.length; g++ ) {
590 var q = 'input.inspector__input.wpbc_sr_only[data-style-key="' + keys[g] + '"][data-col-idx="' + idx + '"]';
591 var inputs = host.querySelectorAll( q );
592 for ( var n = 0; n < inputs.length; n++ ) {
593 var lbl = inputs[n] && inputs[n].nextElementSibling;
594 if ( lbl && lbl.classList && lbl.classList.contains( 'wpbc_bfb__chip' ) ) {
595 if ( enable ) {
596 lbl.classList.add( 'wpbc_do_rotate_90' );
597 } else {
598 lbl.classList.remove( 'wpbc_do_rotate_90' );
599 }
600 }
601 }
602 }
603 }
604
605 /**
606 * Apply rotation class to *all* columns, using the effective `dir` value
607 * (saved value or default from COL_PROPS).
608 */
609 function sync_axis_rotation_all() {
610 var def = get_defaults_obj(); // includes def.dir (which is 'column' in your code).
611 for ( var i = 0; i < styles_arr.length; i++ ) {
612 var dir_val = ( styles_arr[i] && styles_arr[i].dir ) ? String( styles_arr[i].dir ) : String( def.dir );
613 var enable = ( dir_val === 'column' );
614 toggle_axis_rotation_for_col( i, enable );
615 }
616 }
617
618 // Delay (ms) for deferred UI updates after changing layout combo.
619 var rerender_delay_ms = 420;
620
621 /**
622 * Schedule icon rotation + re-render with optional immediate rotation.
623 *
624 * @param {number} col_idx 0-based column index
625 * @param {string} new_dir "row" | "column"
626 * @param {{delay?:number, rotate_now?:boolean}} [opts]
627 */
628 function schedule_rerender(col_idx, new_dir, opts) {
629 opts = opts || {};
630 var delay = (typeof opts.delay === 'number') ? opts.delay : rerender_delay_ms;
631
632 // Avoid stacked timers if the user clicks quickly.
633 if ( host.__rerender_timer ) {
634 clearTimeout( host.__rerender_timer );
635 }
636
637 // Optional immediate feedback (used by plain "dir" radios).
638 if ( opts.rotate_now ) {
639 toggle_axis_rotation_for_col( col_idx, String( new_dir ) === 'column' );
640 }
641
642 host.__rerender_timer = setTimeout( function () {
643 // If we didn't rotate immediately, do it now (used by combo).
644 if ( ! opts.rotate_now ) {
645 toggle_axis_rotation_for_col( col_idx, String( new_dir ) === 'column' );
646 }
647 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
648 host.__rerender_timer = null;
649 }, delay );
650 }
651
652
653 function commit(builder, section_el, styles_arr) {
654 // Decide activation.
655 var should_activate = styles_has_any_non_default( styles_arr, get_defaults_obj );
656 if ( should_activate ) {
657 section_el.setAttribute( 'data-colstyles-active', '1' );
658 } else {
659 section_el.removeAttribute( 'data-colstyles-active' );
660 }
661
662 // Normalize length to current number of columns (keeps attribute tidy).
663 styles_arr.length = dom_cols( section_el );
664
665 // Persist minimal JSON (omit defaults/empties).
666 var save_arr = strip_defaults_for_save( styles_arr, get_defaults_obj );
667 var json = UI.WPBC_BFB_Column_Styles.stringify_col_styles( save_arr );
668 section_el.setAttribute( 'data-col_styles', json );
669 if ( section_el.dataset ) {
670 section_el.dataset.col_styles = json;
671 }
672
673 // Live preview (mini + gated real columns).
674 UI.WPBC_BFB_Column_Styles.apply( section_el, styles_arr );
675
676 // Notify listeners.
677 if ( builder && builder.bus && Core.WPBC_BFB_Events ) {
678 builder.bus.emit && builder.bus.emit( Core.WPBC_BFB_Events.STRUCTURE_CHANGE, {
679 source: 'column_styles',
680 field : section_el
681 } );
682 }
683 }
684
685
686 function on_change( e ) {
687 var t = e.target;
688 // Radios fire both 'input' and 'change' in most browsers.
689 if (t && t.type === 'radio' && e.type === 'input') return;
690
691 var key = t && t.getAttribute( 'data-style-key' );
692 if ( ! key || ( ! is_supported_key( key ) && key !== 'layout_combo' ) ) { return; }
693
694 var idx = parseInt( t.getAttribute( 'data-col-idx' ), 10 ) || 0;
695
696 // layout_combo: commit now, rotate + re-render later (no immediate icon change).
697 if ( key === 'layout_combo' ) {
698 var parts = String( t.value || '' ).split( '|' );
699 var dir = parts[0] || 'row';
700 var wrap = parts[1] || 'nowrap';
701
702 styles_arr[idx].dir = normalize_value( 'dir', dir );
703 styles_arr[idx].wrap = normalize_value( 'wrap', wrap );
704 commit( builder, section_el, styles_arr );
705
706 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
707 return;
708 }
709
710 // Existing: GAP pair branch.
711 if ( key === 'gap' ) {
712 var numEl = host.querySelector( '[data-style-key="gap"][data-style-part="value"][data-col-idx="' + idx + '"]' );
713 var unitEl = host.querySelector( '[data-style-key="gap"][data-style-part="unit"][data-col-idx="' + idx + '"]' );
714 var num = numEl ? String( numEl.value || '' ).trim() : '';
715 var unit = unitEl ? String( unitEl.value || 'px' ).trim() : 'px';
716 var raw = num ? ( num + unit ) : '';
717 styles_arr[idx].gap = normalize_value( 'gap', raw );
718 commit( builder, section_el, styles_arr );
719 return;
720 }
721
722 // dir: commit now, rotate immediately for snappy feedback, still re-render after delay.
723 if ( key === 'dir' ) {
724 styles_arr[idx].dir = normalize_value( 'dir', t.value );
725 commit( builder, section_el, styles_arr );
726
727 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
728 return;
729 }
730
731 // Default branch (unchanged).
732 styles_arr[idx][key] = normalize_value( key, t.value );
733 commit( builder, section_el, styles_arr );
734 }
735
736
737 function on_click( e ) {
738 var btn = e.target.closest( '[data-action="colstyles-reset"]' );
739 if ( ! btn ) { return; }
740
741 // Clear dataset + activation flag and remove inline vars
742 section_el.removeAttribute( 'data-colstyles-active' );
743 section_el.removeAttribute( 'data-col_styles' );
744 if ( section_el.dataset ) { delete section_el.dataset.col_styles; }
745
746 UI.WPBC_BFB_Column_Styles.apply( section_el, [] );
747
748 // Re-render fresh, not persisted
749 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
750
751 if ( builder && builder.bus && Core.WPBC_BFB_Events ) {
752 builder.bus.emit && builder.bus.emit( Core.WPBC_BFB_Events.STRUCTURE_CHANGE, { source : 'column_styles_reset', field : section_el } );
753 }
754 }
755
756 host.addEventListener( 'input', on_change, true );
757 host.addEventListener( 'change', on_change, true );
758 host.addEventListener( 'click', on_click, true );
759
760 // Initial apply (does NOT auto-activate).
761 UI.WPBC_BFB_Column_Styles.apply( section_el, styles_arr );
762
763 // Provide cleanup to avoid leaks.
764 host.__wpbc_cleanup = function () {
765 try {
766 host.removeEventListener( 'input', on_change, true );
767 host.removeEventListener( 'change', on_change, true );
768 host.removeEventListener( 'click', on_click, true );
769 } catch ( _e ) {}
770 };
771 },
772
773 /**
774 * Refresh the mounted editor after columns count changes.
775 *
776 * @param {object} builder
777 * @param {HTMLElement} section_el
778 * @param {HTMLElement} inspector_root
779 */
780 refresh_for_section : function ( builder, section_el, inspector_root ) {
781 var host = inspector_root && inspector_root.querySelector( '[data-bfb-slot="column_styles"]' );
782 if ( ! host ) { return; }
783 this.render_for_section( builder, section_el, host );
784 }
785 };
786
787 // Optional: register a factory slot for environments that use inspector factory.
788 w.wpbc_bfb_inspector_factory_slots = w.wpbc_bfb_inspector_factory_slots || {};
789 w.wpbc_bfb_inspector_factory_slots.column_styles = function ( host, opts ) {
790 try {
791 var builder = ( opts && opts.builder ) || w.wpbc_bfb || null;
792 var section_el = ( opts && opts.el ) || ( builder && builder.get_selected_field && builder.get_selected_field() ) || null;
793 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
794 } catch ( e ) {
795 w._wpbc && w._wpbc.dev && w._wpbc.dev.error && w._wpbc.dev.error( 'wpbc_bfb_inspector_factory_slots.column_styles', e );
796 }
797 };
798
799 })( window );
800