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

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

1,253 lines 49.9 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 * padding: { var: '--wpbc-bfb-col-padding', 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 * Authoritative CSS: ../includes/page-form-builder/__css/bfb_fields.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-padding, 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-padding, 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="padding" 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 length controls (number + unit)
58 *
59 * If your new length style is a pair (value + unit), mirror the 'gap' pattern:
60 * - two inputs with data-style-part="value" and data-style-part="unit"
61 * - use normalize: 'len' in COL_PROPS
62 * The shared on_change path combines and normalizes every registered split length control.
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) both controls use data-style-part and the registry uses normalize: 'len'
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 2026-09-19 16:30
90 * @version 1.1.1
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 or enum definition (`id`, `len`, `max_len`, or allowlisted values).
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 padding: { var: '--wpbc-bfb-col-padding', def: '0px', normalize: 'len' },
124 margin : { var: '--wpbc-bfb-col-margin', def: '0px', normalize: 'len' },
125 padding_top : { var: '--wpbc-bfb-col-padding-top', def: '0px', normalize: 'len' },
126 padding_right : { var: '--wpbc-bfb-col-padding-right', def: '0px', normalize: 'len' },
127 padding_bottom: { var: '--wpbc-bfb-col-padding-bottom', def: '0px', normalize: 'len' },
128 padding_left : { var: '--wpbc-bfb-col-padding-left', def: '0px', normalize: 'len' },
129 margin_top : { var: '--wpbc-bfb-col-margin-top', def: '0.7em', normalize: 'len' },
130 margin_right : { var: '--wpbc-bfb-col-margin-right', def: '0px', normalize: 'len' },
131 margin_bottom : { var: '--wpbc-bfb-col-margin-bottom', def: '0.7em', normalize: 'len' },
132 margin_left : { var: '--wpbc-bfb-col-margin-left', def: '0px', normalize: 'len' },
133 max_width : { var: '--wpbc-bfb-col-max-width', def: 'none', normalize: 'max_len' },
134 max_height : { var: '--wpbc-bfb-col-max-height', def: 'none', normalize: 'max_len' },
135 overflow : {
136 var: '--wpbc-bfb-col-overflow',
137 def: 'visible',
138 normalize: { type: 'enum', values: [ 'visible', 'auto', 'hidden' ] }
139 },
140 overflow_x : {
141 var: '--wpbc-bfb-col-overflow-x',
142 def: 'visible',
143 normalize: { type: 'enum', values: [ 'visible', 'auto', 'hidden' ] }
144 },
145 overflow_y : {
146 var: '--wpbc-bfb-col-overflow-y',
147 def: 'visible',
148 normalize: { type: 'enum', values: [ 'visible', 'auto', 'hidden' ] }
149 },
150 aself: { var: '--wpbc-bfb-col-aself', def: 'flex-start',
151 normalize: { type: 'enum', values: [ 'flex-start', 'center', 'flex-end', 'stretch' ] }
152 }
153 // Example additions:
154 // pad : { var: '--wpbc-bfb-col-pad', def: '0px', normalize: 'len' }
155 };
156
157 var BOX_SIDE_GROUPS = {
158 padding: [ 'padding_top', 'padding_right', 'padding_bottom', 'padding_left' ],
159 margin : [ 'margin_top', 'margin_right', 'margin_bottom', 'margin_left' ]
160 };
161
162 var BOX_SIDE_KEYS = BOX_SIDE_GROUPS.padding.concat( BOX_SIDE_GROUPS.margin );
163 var OVERFLOW_AXIS_KEYS = [ 'overflow_x', 'overflow_y' ];
164
165 /**
166 * Normalize a "length-like" value (e.g., "8" → "8px").
167 * Accepts px/rem/em/%; expand the regex if you allow more units.
168 *
169 * @param {string|number} v
170 * @returns {string} normalized value (always non-empty)
171 */
172 function norm_len( v ) {
173 var sv = String( v || '' ).trim();
174 if ( ! sv ) { return '0px'; }
175 if ( /^\d+(\.\d+)?$/.test( sv ) ) { return sv + 'px'; } // number -> px.
176 if ( /^\d+(\.\d+)?(px|rem|em|%)$/.test( sv ) ) { return sv; } // allowed units.
177 return '0px';
178 }
179
180 /**
181 * Normalize an optional maximum dimension.
182 *
183 * Empty values and `none` remove the limit. Numeric values accept the units
184 * exposed by the Inspector and reject arbitrary CSS declarations.
185 *
186 * @param {string|number} candidate_value Candidate maximum dimension.
187 * @returns {string} Valid CSS maximum dimension or `none`.
188 */
189 function norm_max_len( candidate_value ) {
190 var normalized_value = String( candidate_value == null ? '' : candidate_value ).trim();
191 if ( ! normalized_value || 'none' === normalized_value ) {
192 return 'none';
193 }
194 if ( /^\d+(\.\d+)?$/.test( normalized_value ) ) {
195 return normalized_value + 'px';
196 }
197 if ( /^\d+(\.\d+)?(px|rem|em|%|vh|vw)$/.test( normalized_value ) ) {
198 return normalized_value;
199 }
200 return 'none';
201 }
202
203 /**
204 * Example: ac: { var: '--wpbc-bfb-col-ac', def: 'normal', normalize: { type: 'enum', values: ['normal','stretch','center','start','end','space-between','space-around','space-evenly'] } }
205 *
206 * @param v
207 * @param vals
208 * @returns {string|*}
209 */
210 function norm_enum(v, vals) {
211 v = String( v || '' );
212 return vals.indexOf( v ) !== -1 ? v : vals[0];
213 }
214
215 /**
216 * Normalizer registry. Extend to add custom validators/transforms
217 * (e.g., enums, numbers with ranges, etc.).
218 */
219 var NORMALIZE = {
220 id : v => String( v || '' ),
221 len : norm_len,
222 max_len: norm_max_len,
223 enum : (v, values) => norm_enum( v, values )
224 };
225
226 /**
227 * Check whether a style key is supported by COL_PROPS.
228 *
229 * @param {string} k
230 * @returns {boolean}
231 */
232 function is_supported_key( k ) {
233 return Object.prototype.hasOwnProperty.call( COL_PROPS, k );
234 }
235
236 /**
237 * Normalize a value for a given style key using its configured normalizer.
238 *
239 * @param {string} key
240 * @param {any} val
241 * @returns {string}
242 */
243 function normalize_value(key, val) {
244 var cfg = COL_PROPS[key];
245 if ( ! cfg ) {
246 return String( val || '' );
247 }
248 if ( cfg.normalize && typeof cfg.normalize === 'object' && cfg.normalize.type === 'enum' ) {
249 return NORMALIZE.enum( val, cfg.normalize.values || [] );
250 }
251 var fn = NORMALIZE[cfg.normalize] || NORMALIZE.id;
252 return fn( val );
253 }
254
255 /**
256 * Build a plain object containing defaults for all supported style keys.
257 *
258 * @returns {Record<string, string>}
259 */
260 function get_defaults_obj() {
261 var o = {}; for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) { o[k] = COL_PROPS[k].def; } }
262 return o;
263 }
264
265 /**
266 * Resolve one padding or margin side from explicit or legacy shorthand data.
267 *
268 * @param {Record<string, string>} source_style Saved sparse column styles.
269 * @param {string} side_key Explicit side key.
270 * @param {string} legacy_key Legacy shorthand key.
271 * @param {Record<string, string>} defaults Defaults for supported styles.
272 * @returns {string} Effective normalized side length.
273 */
274 function get_effective_box_side( source_style, side_key, legacy_key, defaults ) {
275 if ( Object.prototype.hasOwnProperty.call( source_style, side_key ) ) {
276 return normalize_value( side_key, source_style[ side_key ] );
277 }
278 if ( Object.prototype.hasOwnProperty.call( source_style, legacy_key ) ) {
279 return normalize_value( legacy_key, source_style[ legacy_key ] );
280 }
281 return defaults[ side_key ];
282 }
283
284 /**
285 * Return the padding or margin group that owns an explicit side key.
286 *
287 * @param {string} side_key Candidate side key.
288 * @returns {{legacy_key:string, side_keys:Array<string>}|null} Owning group.
289 */
290 function get_box_side_group( side_key ) {
291 for ( var legacy_key in BOX_SIDE_GROUPS ) {
292 if ( Object.prototype.hasOwnProperty.call( BOX_SIDE_GROUPS, legacy_key ) && BOX_SIDE_GROUPS[ legacy_key ].indexOf( side_key ) !== -1 ) {
293 return { legacy_key: legacy_key, side_keys: BOX_SIDE_GROUPS[ legacy_key ] };
294 }
295 }
296 return null;
297 }
298
299 /**
300 * Convert one legacy shorthand group to explicit side values before saving.
301 *
302 * @param {Record<string, string>} style_object Effective column styles.
303 * @param {string} edited_key Side key changed by the user.
304 * @returns {void}
305 */
306 function promote_box_sides_to_explicit( style_object, edited_key ) {
307 var box_group = get_box_side_group( edited_key );
308 if ( ! box_group ) {
309 return;
310 }
311
312 style_object.__has = style_object.__has || {};
313 for ( var side_index = 0; side_index < box_group.side_keys.length; side_index++ ) {
314 var side_key = box_group.side_keys[ side_index ];
315 style_object[ side_key ] = normalize_value( side_key, style_object[ side_key ] );
316 style_object.__has[ side_key ] = true;
317 }
318
319 delete style_object[ box_group.legacy_key ];
320 style_object.__has[ box_group.legacy_key ] = false;
321 }
322
323 /**
324 * Resolve one axis from axis-specific data or the legacy shared overflow.
325 *
326 * @param {Record<string, string>} source_style Saved sparse column styles.
327 * @param {string} axis_key `overflow_x` or `overflow_y`.
328 * @param {Record<string, string>} defaults Registered style defaults.
329 * @returns {string} Effective allowlisted overflow value.
330 */
331 function get_effective_overflow_axis( source_style, axis_key, defaults ) {
332 if ( Object.prototype.hasOwnProperty.call( source_style, axis_key ) ) {
333 return normalize_value( axis_key, source_style[ axis_key ] );
334 }
335 if ( Object.prototype.hasOwnProperty.call( source_style, 'overflow' ) ) {
336 return normalize_value( axis_key, source_style.overflow );
337 }
338 return defaults[ axis_key ];
339 }
340
341 /**
342 * Promote a legacy shared overflow to explicit X and Y values on first edit.
343 *
344 * @param {Record<string, *>} style_object Mutable full style object.
345 * @returns {void}
346 */
347 function promote_overflow_axes_to_explicit( style_object ) {
348 if ( ! style_object || ! style_object.__has || true !== style_object.__has.overflow ) {
349 return;
350 }
351
352 for ( var axis_index = 0; axis_index < OVERFLOW_AXIS_KEYS.length; axis_index++ ) {
353 var axis_key = OVERFLOW_AXIS_KEYS[ axis_index ];
354 style_object[ axis_key ] = normalize_value( axis_key, style_object[ axis_key ] );
355 style_object.__has[ axis_key ] = true;
356 }
357
358 delete style_object.overflow;
359 style_object.__has.overflow = false;
360 }
361
362 /**
363 * Check whether an explicit default must remain in sparse saved JSON.
364 *
365 * @param {string} style_key Registered style key.
366 * @param {Record<string, boolean>} presence_map Saved-key presence map.
367 * @returns {boolean} Whether to preserve the explicit default value.
368 */
369 function should_keep_explicit_default( style_key, presence_map ) {
370 if ( ! presence_map || true !== presence_map[ style_key ] ) {
371 return false;
372 }
373 return BOX_SIDE_KEYS.indexOf( style_key ) !== -1;
374 }
375
376 /**
377 * Reset allowlisted keys on one effective Inspector style object.
378 *
379 * The object is mutated intentionally. Legacy shorthand data is promoted
380 * first so resetting one side or axis preserves every unrelated effective
381 * value. Reset keys are marked absent for sparse persistence.
382 *
383 * @param {Record<string, *>} style_object Effective mutable column styles.
384 * @param {Array<string>} requested_keys Candidate registered style keys.
385 * @returns {Array<string>} Supported keys reset to their registered defaults.
386 */
387 function reset_style_keys_to_defaults( style_object, requested_keys ) {
388 var style_keys = [];
389 if ( ! style_object || ! Array.isArray( requested_keys ) ) {
390 return style_keys;
391 }
392
393 for ( var requested_index = 0; requested_index < requested_keys.length; requested_index++ ) {
394 if ( is_supported_key( requested_keys[ requested_index ] ) ) {
395 style_keys.push( requested_keys[ requested_index ] );
396 }
397 }
398 if ( ! style_keys.length ) {
399 return style_keys;
400 }
401
402 style_object.__has = style_object.__has || {};
403 for ( var promotion_index = 0; promotion_index < style_keys.length; promotion_index++ ) {
404 var promotion_key = style_keys[ promotion_index ];
405 if ( BOX_SIDE_KEYS.indexOf( promotion_key ) !== -1 ) {
406 promote_box_sides_to_explicit( style_object, promotion_key );
407 }
408 if ( OVERFLOW_AXIS_KEYS.indexOf( promotion_key ) !== -1 ) {
409 promote_overflow_axes_to_explicit( style_object );
410 }
411 }
412
413 for ( var reset_index = 0; reset_index < style_keys.length; reset_index++ ) {
414 var style_key = style_keys[ reset_index ];
415 style_object[ style_key ] = COL_PROPS[ style_key ].def;
416 style_object.__has[ style_key ] = false;
417 }
418
419 return style_keys;
420 }
421
422 /**
423 * Apply all CSS variables (from COL_PROPS) onto a node based on a style object.
424 * Missing/empty values fall back to defaults.
425 *
426 * @param {HTMLElement} node
427 * @param {Record<string, string>} style_obj
428 */
429 function set_vars( node, style_obj ) {
430 if ( ! node ) { return; }
431 for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) {
432 var cssVar = COL_PROPS[k].var;
433 var v = ( style_obj && style_obj[k] != null && String( style_obj[k] ).trim() !== '' ) ? style_obj[k] : COL_PROPS[k].def;
434 node.style.setProperty( cssVar, normalize_value( k, v ) );
435 }}
436 }
437
438 function set_vars_sparse(node, style_obj) {
439 if ( !node ) return;
440 for ( var k in COL_PROPS ) { if ( is_supported_key(k) ) {
441 var cssVar = COL_PROPS[k].var;
442 if ( style_obj && Object.prototype.hasOwnProperty.call(style_obj, k) && String(style_obj[k]).trim() !== '' ) {
443 node.style.setProperty(cssVar, normalize_value( k, style_obj[k] ));
444 } else {
445 // important: remove var instead of writing a default
446 node.style.removeProperty(cssVar);
447 }
448 }}
449 }
450
451 /**
452 * Remove all CSS variables (from COL_PROPS) from a node.
453 *
454 * @param {HTMLElement} node
455 */
456 function clear_vars( node ) {
457 if ( ! node ) { return; }
458 for ( var k in COL_PROPS ) { if ( is_supported_key( k ) ) {
459 node.style.removeProperty( COL_PROPS[k].var );
460 }}
461 }
462
463 /**
464 * Clamp helper for columns number.
465 *
466 * @param {number|string} n
467 * @returns {number}
468 */
469 function clamp_cols( n ) {
470 return ( S.clamp ? S.clamp( Number( n ) || 1, 1, 4 ) : Math.max( 1, Math.min( 4, Number( n ) || 1 ) ) );
471 }
472
473 /**
474 * Read actual number of columns from DOM.
475 *
476 * @param {HTMLElement} el
477 * @returns {number}
478 */
479 function dom_cols( el ) {
480 try {
481 var row = el ? el.querySelector( ':scope > ' + DOM.row ) : null;
482 var cnt = row ? row.querySelectorAll( ':scope > ' + DOM.column ).length : 1;
483 return clamp_cols( cnt );
484 } catch ( _e ) {
485 return 1;
486 }
487 }
488
489 // ------------------------------------------------------------------------------------------------
490 // Service: Column Styles
491 // ------------------------------------------------------------------------------------------------
492 UI.WPBC_BFB_Column_Styles = {
493
494 /**
495 * Reset a supported subset of one effective Inspector style object.
496 *
497 * @param {Record<string, *>} style_object Effective mutable column styles.
498 * @param {Array<string>} style_keys Registered keys to reset.
499 * @returns {Array<string>} Supported keys that were reset.
500 */
501 reset_style_keys : function ( style_object, style_keys ) {
502 return reset_style_keys_to_defaults( style_object, style_keys );
503 },
504
505 /**
506 * Highlight a specific column in both the live canvas and the mini preview,
507 * and store the active index on the section.
508 *
509 * @param {HTMLElement} section_el
510 * @param {number|string} key_1based 1-based column index (clamped)
511 */
512 set_selected_col_flag : function ( section_el, key_1based ) {
513 if ( ! section_el ) { return; }
514 var cols_cnt = dom_cols( section_el );
515 var idx = Math.min( Math.max( parseInt( key_1based, 10 ) || 1, 1 ), cols_cnt || 1 );
516 var idx0 = idx - 1;
517
518 section_el.setAttribute( 'data-selected-col', String( idx ) );
519
520 // Real canvas columns.
521 var row = section_el.querySelector( ':scope > ' + DOM.row );
522 var cols = row ? row.querySelectorAll( ':scope > ' + DOM.column ) : [];
523 for ( var i = 0; i < cols.length; i++ ) {
524 if ( cols[i].classList ) {
525 cols[i].classList.toggle( 'is-selected-column', i === idx0 );
526 }
527 }
528
529 // Mini preview columns.
530 var pcols = section_el.querySelectorAll( ':scope .wpbc_bfb__section__cols > .wpbc_bfb__section__col' );
531 for ( var j = 0; j < pcols.length; j++ ) {
532 if ( pcols[j].classList ) {
533 pcols[j].classList.toggle( 'is-selected-column', j === idx0 );
534 }
535 }
536 },
537
538 /**
539 * Remove column selection highlight from both canvas and mini preview.
540 *
541 * @param {HTMLElement} section_el
542 */
543 clear_selected_col_flag : function ( section_el ) {
544 if ( ! section_el ) { return; }
545 section_el.removeAttribute( 'data-selected-col' );
546
547 var row = section_el.querySelector( ':scope > ' + DOM.row );
548 var cols = row ? row.querySelectorAll( ':scope > ' + DOM.column ) : [];
549 for ( var i = 0; i < cols.length; i++ ) {
550 cols[i].classList && cols[i].classList.remove( 'is-selected-column' );
551 }
552
553 var pcols = section_el.querySelectorAll( ':scope .wpbc_bfb__section__cols > .wpbc_bfb__section__col' );
554 for ( var j = 0; j < pcols.length; j++ ) {
555 pcols[j].classList && pcols[j].classList.remove( 'is-selected-column' );
556 }
557 },
558
559
560
561 /**
562 * Parse JSON string to array of per-column style objects.
563 *
564 * @param {string} s
565 * @returns {Array<{dir:string,wrap:string,jc:string,ai:string,gap:string,padding:string,margin:string,padding_top:string,padding_right:string,padding_bottom:string,padding_left:string,margin_top:string,margin_right:string,margin_bottom:string,margin_left:string,max_width:string,max_height:string,overflow:string,overflow_x:string,overflow_y:string,aself:string}>}
566 */
567 parse_col_styles : function ( s ) {
568 if ( ! s ) { return []; }
569 var obj = ( S.safe_json_parse ? S.safe_json_parse( String( s ), null ) : ( function(){ try { return JSON.parse( String( s ) ); } catch( _e ){ return null; } } )() );
570 if ( Array.isArray( obj ) ) { return obj; }
571 if ( obj && typeof obj === 'object' && Array.isArray( obj.columns ) ) { return obj.columns; }
572 return [];
573 },
574
575 /**
576 * Stringify styles array to canonical JSON.
577 *
578 * @param {Array} arr
579 * @returns {string}
580 */
581 stringify_col_styles : function ( arr ) {
582 var data = Array.isArray( arr ) ? arr : [];
583 return ( S.stringify_data_value ? S.stringify_data_value( data ) : JSON.stringify( data ) );
584 },
585
586 /**
587 * Check if per-column styles are active for a section.
588 * - Active if element flag data-colstyles-active="1" OR non-empty serialized styles.
589 *
590 * @param {HTMLElement} section_el
591 * @returns {boolean}
592 */
593 is_active : function ( section_el ) {
594 if ( ! section_el ) { return false; }
595 if ( section_el.getAttribute( 'data-colstyles-active' ) === '1' ) { return true; }
596 var raw = section_el.getAttribute( 'data-col_styles' ) || ( section_el.dataset ? ( section_el.dataset.col_styles || '' ) : '' );
597 var arr = this.parse_col_styles( raw );
598 var DEF = get_defaults_obj();
599 // Active only if any column has a non-default, non-empty override.
600 for ( var i = 0; i < arr.length; i++ ) {
601 var s = arr[i] || {};
602 for ( var k in DEF ) {
603 if ( Object.prototype.hasOwnProperty.call( s, k ) ) {
604 var v = String( s[k] );
605 if ( v && v !== String( DEF[k] ) ) { return true; }
606 }
607 }
608 }
609 return false;
610 },
611
612 /**
613 * Apply per-column styles to preview and, when active, to real columns.
614 *
615 * @param {HTMLElement} section_el
616 * @param {Array} styles
617 */
618 apply : function ( section_el, styles ) {
619 if ( ! section_el ) { return; }
620
621 // Mini preview inside the section template (always on).
622 var preview = section_el.querySelector( ':scope .wpbc_bfb__section__cols' );
623 if ( preview ) {
624 var pcols = preview.querySelectorAll( ':scope > .wpbc_bfb__section__col' );
625 for ( var i = 0; i < pcols.length; i++ ) {
626 set_vars(pcols[i], styles[i] || {}); // OK to use defaults in preview
627 }
628 }
629
630 // Determine activation from current element state (not from styles arg alone).
631 var active = this.is_active( section_el );
632
633 // If not active, clean up inline vars and remove the flag.
634 if ( ! active ) {
635 section_el.removeAttribute( 'data-colstyles-active' );
636 var row_off = section_el.querySelector( ':scope > ' + DOM.row );
637 if ( row_off ) {
638 var nodes = row_off.querySelectorAll( ':scope > ' + DOM.column );
639 for ( var j = 0; j < nodes.length; j++ ) {
640 clear_vars( nodes[j] );
641 }
642 }
643 return;
644 }
645
646 // Active: add flag if missing and write CSS vars to REAL canvas columns.
647 section_el.setAttribute( 'data-colstyles-active', '1' );
648
649 // NEW: always use the sparse, saved JSON for REAL columns.
650 var use_styles = this.parse_col_styles(
651 section_el.getAttribute( 'data-col_styles' ) ||
652 (section_el.dataset ? (section_el.dataset.col_styles || '') : '')
653 );
654
655 var row = section_el.querySelector( ':scope > ' + DOM.row );
656 if ( row ) {
657 var rcols = row.querySelectorAll( ':scope > ' + DOM.column );
658 for ( var k = 0; k < rcols.length; k++ ) {
659 set_vars_sparse(rcols[k], use_styles[k] || {}); // only write what exists
660 }
661 }
662 }
663 };
664
665 // ------------------------------------------------------------------------------------------------
666 // Inspector component (slot: "column_styles")
667 // ------------------------------------------------------------------------------------------------
668 UI.wpbc_bfb_column_styles = {
669
670 /**
671 * Render the per-column style editor (wp.template: 'wpbc-bfb-column-styles').
672 *
673 * @param {object} builder
674 * @param {HTMLElement} section_el
675 * @param {HTMLElement} host
676 */
677 render_for_section : function ( builder, section_el, host ) {
678 if ( ! host || ! section_el ) { return; }
679
680 // Capture current active tab BEFORE we clear host.
681 var __prev_root = host.querySelector( '[data-wpbc-tabs]' );
682 var ds = section_el.dataset || {};
683 var __prev_key = (__prev_root && __prev_root.getAttribute( 'data-wpbc-tab-active' )) || host.__wpbc_active_key || ds.col_styles_active_tab || null;
684 var column_control_tab_state = host.__wpbc_column_control_tab_state || {};
685 var previous_control_tabs = host.querySelectorAll( '[data-column-control-tabs]' );
686 for ( var previous_control_index = 0; previous_control_index < previous_control_tabs.length; previous_control_index++ ) {
687 var previous_control_root = previous_control_tabs[ previous_control_index ];
688 var previous_control_column = ( parseInt( previous_control_root.getAttribute( 'data-col-idx' ), 10 ) || 0 ) + 1;
689 var previous_control_group = previous_control_root.getAttribute( 'data-column-control-tabs' ) || '';
690 var previous_control_tab = previous_control_root.getAttribute( 'data-column-control-tab-active' ) || '';
691 if ( previous_control_group && previous_control_tab ) {
692 column_control_tab_state[ previous_control_column + '_' + previous_control_group ] = previous_control_tab;
693 }
694 }
695 host.__wpbc_column_control_tab_state = column_control_tab_state;
696
697 // Cleanup previous mount and clear.
698 if ( host.__wpbc_cleanup ) {
699 try { host.__wpbc_cleanup(); } catch ( _e ) {}
700 host.__wpbc_cleanup = null;
701 }
702 host.innerHTML = '';
703
704 var tpl = ( w.wp && w.wp.template ) ? w.wp.template( 'wpbc-bfb-column-styles' ) : null;
705 if ( ! tpl ) { return; }
706
707 var col_count = dom_cols( section_el );
708 var raw_json = section_el.getAttribute( 'data-col_styles' ) || ( section_el.dataset ? ( section_el.dataset.col_styles || '' ) : '' );
709
710 var saved_arr = UI.WPBC_BFB_Column_Styles.parse_col_styles( raw_json );
711 var styles_arr = [];
712
713 // Normalize length to current columns (UI-only defaults do NOT auto-activate).
714 var def = get_defaults_obj();
715 for ( var i = 0; i < col_count; i++ ) {
716 var src = saved_arr[i] || {};
717 // Merge for display, but track which keys were actually present in saved JSON.
718 var full = Object.assign( {}, def, src );
719 full.max_width = normalize_value( 'max_width', full.max_width );
720 full.max_height = normalize_value( 'max_height', full.max_height );
721 full.overflow = normalize_value( 'overflow', full.overflow );
722 full.overflow_x = get_effective_overflow_axis( src, 'overflow_x', def );
723 full.overflow_y = get_effective_overflow_axis( src, 'overflow_y', def );
724 for ( var legacy_box_key in BOX_SIDE_GROUPS ) {
725 if ( ! Object.prototype.hasOwnProperty.call( BOX_SIDE_GROUPS, legacy_box_key ) ) {
726 continue;
727 }
728 for ( var side_index = 0; side_index < BOX_SIDE_GROUPS[ legacy_box_key ].length; side_index++ ) {
729 var side_key = BOX_SIDE_GROUPS[ legacy_box_key ][ side_index ];
730 full[ side_key ] = get_effective_box_side( src, side_key, legacy_box_key, def );
731 }
732 }
733 full.__has = {};
734 for ( var style_key in def ) {
735 if ( Object.prototype.hasOwnProperty.call( def, style_key ) ) {
736 full.__has[ style_key ] = Object.prototype.hasOwnProperty.call( src, style_key );
737 }
738 }
739 styles_arr[i] = full;
740 }
741 styles_arr.length = col_count; // clamp.
742
743 host.innerHTML = tpl( {
744 cols : col_count,
745 styles : styles_arr,
746 active : UI.WPBC_BFB_Column_Styles.is_active( section_el ),
747 control_tabs: column_control_tab_state
748 } );
749
750 var column_control_tab_roots = host.querySelectorAll( '[data-column-control-tabs]' );
751 for ( var control_root_index = 0; control_root_index < column_control_tab_roots.length; control_root_index++ ) {
752 var control_root = column_control_tab_roots[ control_root_index ];
753 activate_column_control_tab( control_root, control_root.getAttribute( 'data-column-control-tab-active' ) || '', false );
754 }
755
756 if ( window.wpbc_ui_tabs && host ) {
757 window.wpbc_ui_tabs.init_on( host );
758
759
760 // Persist the active tab so we can restore it after re-renders.
761 var tabsRoot = host.querySelector( '[data-wpbc-tabs]' );
762 if ( tabsRoot && !tabsRoot.__wpbc_persist_listener ) {
763 tabsRoot.__wpbc_persist_listener = true;
764 tabsRoot.addEventListener( 'wpbc:tabs:change', function (e) {
765 var k = e && e.detail && e.detail.active_key;
766 if ( k ) {
767 host.__wpbc_active_key = String( k );
768 if ( section_el && section_el.dataset ) {
769 section_el.dataset.col_styles_active_tab = String( k );
770 }
771 // NEW: reflect selection on the section + columns.
772 UI.WPBC_BFB_Column_Styles.set_selected_col_flag( section_el, k );
773 }
774 }, true );
775
776 }
777
778 // Restore previous tab if it still exists (clamp to new count).
779 var __key
780 if ( __prev_key ) {
781 var __new_root = host.querySelector( '[data-wpbc-tabs]' );
782 __key = String( Math.min( Math.max( parseInt( __prev_key, 10 ) || 1, 1 ), col_count ) );
783 if ( __new_root && window.wpbc_ui_tabs.set_active ) {
784 window.wpbc_ui_tabs.set_active( __new_root, __key );
785 }
786 }
787
788 // After restoring the tab, ensure highlight matches the active tab.
789 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');
790 UI.WPBC_BFB_Column_Styles.set_selected_col_flag( section_el, __active_key_now );
791 }
792
793 // Re-wire number - range pairing (ValueSlider) for freshly rendered controls.
794 try {
795 UI.InspectorEnhancers && UI.InspectorEnhancers.scan && UI.InspectorEnhancers.scan( host );
796 // Alternatively (direct wiring):
797 // UI.WPBC_BFB_ValueSlider && UI.WPBC_BFB_ValueSlider.init_on && UI.WPBC_BFB_ValueSlider.init_on( host );
798 } catch ( _e ) {}
799
800 // Inspector markup is replaced on every render, so initialize tooltips for the fresh help controls.
801 if ( 'function' === typeof w.wpbc_define_tippy_tooltips ) {
802 w.wpbc_define_tippy_tooltips( '[data-bfb-slot="column_styles"] ' );
803 }
804
805 // Set initial state of ICONS (including defaults) is correct.
806 sync_axis_rotation_all();
807
808 function styles_has_any_non_default(styles_arr, get_defaults_obj_fn) {
809 var def = get_defaults_obj_fn();
810 for ( var i = 0; i < styles_arr.length; i++ ) {
811 var s = styles_arr[i] || {};
812 for ( var k in def ) {
813 if ( Object.prototype.hasOwnProperty.call( def, k ) ) {
814 var v = (s[k] == null) ? '' : String( s[k] );
815 // treat empty as "not selected" (not active).
816 if ( v && v !== String( def[k] ) ) {
817 return true;
818 }
819 }
820 }
821 }
822 return false;
823 }
824
825 function strip_defaults_for_save(styles_arr, get_defaults_obj_fn) {
826 var def = get_defaults_obj_fn();
827 var out = [];
828 for ( var i = 0; i < styles_arr.length; i++ ) {
829 var s = styles_arr[i] || {};
830 var row = {};
831 for ( var k in def ) {
832 if ( Object.prototype.hasOwnProperty.call( def, k ) ) {
833 var v = (s[k] == null) ? '' : String( s[k] );
834 var keep_explicit_default = should_keep_explicit_default( k, s.__has );
835 if ( v && ( v !== String( def[k] ) || keep_explicit_default ) ) {
836 row[k] = v; // only keep meaningful overrides.
837 }
838 }
839 }
840 out.push( row );
841 }
842 return out;
843 }
844
845 /**
846 * Toggle rotation class for the chip labels of a specific column and group set.
847 *
848 * @param {number} idx Column index (0-based)
849 * @param {boolean} enable Whether to add (true) or remove (false) the rotation class
850 */
851 function toggle_axis_rotation_for_col( idx, enable ) {
852 var keys = [ 'ai', 'jc' ];
853 for ( var g = 0; g < keys.length; g++ ) {
854 var q = 'input.inspector__input.wpbc_sr_only[data-style-key="' + keys[g] + '"][data-col-idx="' + idx + '"]';
855 var inputs = host.querySelectorAll( q );
856 for ( var n = 0; n < inputs.length; n++ ) {
857 var lbl = inputs[n] && inputs[n].nextElementSibling;
858 if ( lbl && lbl.classList && lbl.classList.contains( 'wpbc_bfb__chip' ) ) {
859 if ( enable ) {
860 lbl.classList.add( 'wpbc_do_rotate_90' );
861 } else {
862 lbl.classList.remove( 'wpbc_do_rotate_90' );
863 }
864 }
865 }
866 }
867 }
868
869 /**
870 * Apply rotation class to *all* columns, using the effective `dir` value
871 * (saved value or default from COL_PROPS).
872 */
873 function sync_axis_rotation_all() {
874 var def = get_defaults_obj(); // includes def.dir (which is 'column' in your code).
875 for ( var i = 0; i < styles_arr.length; i++ ) {
876 var dir_val = ( styles_arr[i] && styles_arr[i].dir ) ? String( styles_arr[i].dir ) : String( def.dir );
877 var enable = ( dir_val === 'column' );
878 toggle_axis_rotation_for_col( i, enable );
879 }
880 }
881
882 /**
883 * Activate one nested column-control tab without affecting Column tabs.
884 *
885 * @param {HTMLElement} control_root Column-control tab group root.
886 * @param {string} tab_key Tab key to activate.
887 * @param {boolean} move_focus Whether focus follows activation.
888 * @returns {void}
889 */
890 function activate_column_control_tab( control_root, tab_key, move_focus ) {
891 if ( ! control_root ) { return; }
892
893 var control_tabs = control_root.querySelectorAll( '[data-column-control-tab-key]' );
894 var control_panels = control_root.querySelectorAll( '[data-column-control-tab-panel]' );
895 var active_tab = null;
896
897 for ( var tab_index = 0; tab_index < control_tabs.length; tab_index++ ) {
898 var control_tab = control_tabs[ tab_index ];
899 var tab_is_active = control_tab.getAttribute( 'data-column-control-tab-key' ) === tab_key;
900 control_tab.setAttribute( 'aria-selected', tab_is_active ? 'true' : 'false' );
901 control_tab.setAttribute( 'tabindex', tab_is_active ? '0' : '-1' );
902 if ( tab_is_active ) {
903 active_tab = control_tab;
904 }
905 }
906
907 for ( var panel_index = 0; panel_index < control_panels.length; panel_index++ ) {
908 var control_panel = control_panels[ panel_index ];
909 if ( control_panel.getAttribute( 'data-column-control-tab-panel' ) === tab_key ) {
910 control_panel.removeAttribute( 'hidden' );
911 } else {
912 control_panel.setAttribute( 'hidden', '' );
913 }
914 }
915
916 control_root.setAttribute( 'data-column-control-tab-active', tab_key );
917 if ( move_focus && active_tab && active_tab.focus ) {
918 active_tab.focus();
919 }
920 }
921
922 /**
923 * Remember one nested tab selection for the current Inspector mount.
924 *
925 * @param {HTMLElement} control_root Column-control tab group root.
926 * @param {string} tab_key Selected tab key.
927 * @returns {void}
928 */
929 function remember_column_control_tab( control_root, tab_key ) {
930 var control_column = ( parseInt( control_root.getAttribute( 'data-col-idx' ), 10 ) || 0 ) + 1;
931 var control_group = control_root.getAttribute( 'data-column-control-tabs' ) || '';
932 if ( control_group ) {
933 column_control_tab_state[ control_column + '_' + control_group ] = tab_key;
934 host.__wpbc_column_control_tab_state = column_control_tab_state;
935 }
936 }
937
938 /**
939 * Reflect whether a nested tab owns any non-default style value.
940 *
941 * @param {number} column_index Zero-based column index.
942 * @param {string} style_key Changed style key.
943 * @param {Record<string, string>} style_object Current full column styles.
944 * @returns {void}
945 */
946 function sync_column_control_tab_marker( column_index, style_key, style_object ) {
947 if ( ! is_supported_key( style_key ) ) { return; }
948
949 var control_tabs = host.querySelectorAll( '[data-column-control-tab-style-keys~="' + style_key + '"][data-col-idx="' + column_index + '"]' );
950 for ( var control_tab_index = 0; control_tab_index < control_tabs.length; control_tab_index++ ) {
951 var control_tab = control_tabs[ control_tab_index ];
952 var owned_style_keys = String( control_tab.getAttribute( 'data-column-control-tab-style-keys' ) || '' ).split( /\s+/ );
953 var control_is_changed = false;
954
955 for ( var owned_key_index = 0; owned_key_index < owned_style_keys.length; owned_key_index++ ) {
956 var owned_style_key = owned_style_keys[ owned_key_index ];
957 if ( is_supported_key( owned_style_key ) && normalize_value( owned_style_key, style_object[ owned_style_key ] ) !== COL_PROPS[ owned_style_key ].def ) {
958 control_is_changed = true;
959 break;
960 }
961 }
962
963 var control_marker = control_tab.querySelector( '[data-column-control-tab-marker]' );
964 var control_changed_text = control_tab.querySelector( '[data-column-control-tab-changed-text]' );
965 control_tab.classList.toggle( 'is-changed', control_is_changed );
966 if ( control_marker ) { control_marker.hidden = ! control_is_changed; }
967 if ( control_changed_text ) { control_changed_text.hidden = ! control_is_changed; }
968 }
969 }
970
971 // Delay (ms) for deferred UI updates after changing layout combo.
972 var rerender_delay_ms = 420;
973
974 /**
975 * Schedule icon rotation + re-render with optional immediate rotation.
976 *
977 * @param {number} col_idx 0-based column index
978 * @param {string} new_dir "row" | "column"
979 * @param {{delay?:number, rotate_now?:boolean}} [opts]
980 */
981 function schedule_rerender(col_idx, new_dir, opts) {
982 opts = opts || {};
983 var delay = (typeof opts.delay === 'number') ? opts.delay : rerender_delay_ms;
984
985 // Avoid stacked timers if the user clicks quickly.
986 if ( host.__rerender_timer ) {
987 clearTimeout( host.__rerender_timer );
988 }
989
990 // Optional immediate feedback (used by plain "dir" radios).
991 if ( opts.rotate_now ) {
992 toggle_axis_rotation_for_col( col_idx, String( new_dir ) === 'column' );
993 }
994
995 host.__rerender_timer = setTimeout( function () {
996 // If we didn't rotate immediately, do it now (used by combo).
997 if ( ! opts.rotate_now ) {
998 toggle_axis_rotation_for_col( col_idx, String( new_dir ) === 'column' );
999 }
1000 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
1001 host.__rerender_timer = null;
1002 }, delay );
1003 }
1004
1005
1006 function commit(builder, section_el, styles_arr) {
1007 // Decide activation.
1008 var should_activate = styles_has_any_non_default( styles_arr, get_defaults_obj );
1009 if ( should_activate ) {
1010 section_el.setAttribute( 'data-colstyles-active', '1' );
1011 } else {
1012 section_el.removeAttribute( 'data-colstyles-active' );
1013 }
1014
1015 // Normalize length to current number of columns (keeps attribute tidy).
1016 styles_arr.length = dom_cols( section_el );
1017
1018 // Persist minimal JSON (omit defaults/empties).
1019 var save_arr = strip_defaults_for_save( styles_arr, get_defaults_obj );
1020 var json = UI.WPBC_BFB_Column_Styles.stringify_col_styles( save_arr );
1021 section_el.setAttribute( 'data-col_styles', json );
1022 if ( section_el.dataset ) {
1023 section_el.dataset.col_styles = json;
1024 }
1025
1026 // Live preview (mini + gated real columns).
1027 UI.WPBC_BFB_Column_Styles.apply( section_el, styles_arr );
1028
1029 // Notify listeners.
1030 if ( builder && builder.bus && Core.WPBC_BFB_Events ) {
1031 builder.bus.emit && builder.bus.emit( Core.WPBC_BFB_Events.STRUCTURE_CHANGE, {
1032 source: 'column_styles',
1033 field : section_el
1034 } );
1035 }
1036 }
1037
1038
1039 function on_change( e ) {
1040 var t = e.target;
1041 // Radios fire both 'input' and 'change' in most browsers.
1042 if (t && t.type === 'radio' && e.type === 'input') return;
1043
1044 var key = t && t.getAttribute( 'data-style-key' );
1045 if ( ! key || ( ! is_supported_key( key ) && key !== 'layout_combo' ) ) { return; }
1046
1047 var idx = parseInt( t.getAttribute( 'data-col-idx' ), 10 ) || 0;
1048
1049 // layout_combo: commit now, rotate + re-render later (no immediate icon change).
1050 if ( key === 'layout_combo' ) {
1051 var parts = String( t.value || '' ).split( '|' );
1052 var dir = parts[0] || 'row';
1053 var wrap = parts[1] || 'nowrap';
1054
1055 styles_arr[idx].dir = normalize_value( 'dir', dir );
1056 styles_arr[idx].wrap = normalize_value( 'wrap', wrap );
1057 commit( builder, section_el, styles_arr );
1058
1059 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
1060 return;
1061 }
1062
1063 // Combine every registered split length control before normalizing it.
1064 var split_normalizer = COL_PROPS[ key ] && COL_PROPS[ key ].normalize;
1065 if ( t.hasAttribute( 'data-style-part' ) && ( 'len' === split_normalizer || 'max_len' === split_normalizer ) ) {
1066 var numEl = host.querySelector( '[data-style-key="' + key + '"][data-style-part="value"][data-col-idx="' + idx + '"]' );
1067 var unitEl = host.querySelector( '[data-style-key="' + key + '"][data-style-part="unit"][data-col-idx="' + idx + '"]' );
1068 var num = numEl ? String( numEl.value || '' ).trim() : '';
1069 var unit = unitEl ? String( unitEl.value || 'px' ).trim() : 'px';
1070 var raw = num ? ( num + unit ) : '';
1071 styles_arr[idx][key] = normalize_value( key, raw );
1072 if ( styles_arr[idx].__has ) {
1073 styles_arr[idx].__has[key] = true;
1074 }
1075 promote_box_sides_to_explicit( styles_arr[idx], key );
1076 commit( builder, section_el, styles_arr );
1077 sync_column_control_tab_marker( idx, key, styles_arr[idx] );
1078 return;
1079 }
1080
1081 // dir: commit now, rotate immediately for snappy feedback, still re-render after delay.
1082 if ( key === 'dir' ) {
1083 styles_arr[idx].dir = normalize_value( 'dir', t.value );
1084 commit( builder, section_el, styles_arr );
1085
1086 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
1087 return;
1088 }
1089
1090 // Default branch (unchanged).
1091 styles_arr[idx][key] = normalize_value( key, t.value );
1092 if ( styles_arr[idx].__has ) {
1093 styles_arr[idx].__has[key] = true;
1094 }
1095 if ( OVERFLOW_AXIS_KEYS.indexOf( key ) !== -1 ) {
1096 promote_overflow_axes_to_explicit( styles_arr[idx] );
1097 }
1098 commit( builder, section_el, styles_arr );
1099 sync_column_control_tab_marker( idx, key, styles_arr[idx] );
1100 }
1101
1102 /**
1103 * Reset an allowlisted subset of one column style object to registered defaults.
1104 *
1105 * Legacy shorthand values are promoted before the reset so clearing one side
1106 * or overflow axis cannot silently change its siblings. The reset keys are
1107 * marked absent so sparse persistence falls back to the canonical defaults.
1108 *
1109 * @param {HTMLElement} reset_control Reset link carrying column and key metadata.
1110 * @returns {boolean} Whether a supported style subset was reset.
1111 */
1112 function reset_column_style_keys( reset_control ) {
1113 var column_index = parseInt( reset_control.getAttribute( 'data-col-idx' ), 10 );
1114 var requested_keys = String( reset_control.getAttribute( 'data-style-keys' ) || '' ).split( /\s+/ );
1115
1116 if ( isNaN( column_index ) || column_index < 0 || column_index >= styles_arr.length ) {
1117 return false;
1118 }
1119
1120 var style_object = styles_arr[ column_index ];
1121 var style_keys = UI.WPBC_BFB_Column_Styles.reset_style_keys( style_object, requested_keys );
1122 if ( ! style_keys.length ) {
1123 return false;
1124 }
1125
1126 var focus_selector = '[data-action="colstyles-reset-keys"][data-col-idx="' + column_index + '"][data-style-keys="' + style_keys.join( ' ' ) + '"]';
1127 commit( builder, section_el, styles_arr );
1128 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
1129
1130 var restored_control = host.querySelector( focus_selector );
1131 if ( restored_control && restored_control.focus ) {
1132 restored_control.focus();
1133 }
1134 return true;
1135 }
1136
1137
1138 function on_click( e ) {
1139 var control_tab = e.target.closest( '[data-column-control-tab-key]' );
1140 if ( control_tab && host.contains( control_tab ) ) {
1141 var control_root = control_tab.closest( '[data-column-control-tabs]' );
1142 var control_tab_key = control_tab.getAttribute( 'data-column-control-tab-key' ) || '';
1143 e.preventDefault();
1144 activate_column_control_tab( control_root, control_tab_key, false );
1145 remember_column_control_tab( control_root, control_tab_key );
1146 return;
1147 }
1148
1149 var property_reset = e.target.closest( '[data-action="colstyles-reset-keys"]' );
1150 if ( property_reset && host.contains( property_reset ) ) {
1151 e.preventDefault();
1152 reset_column_style_keys( property_reset );
1153 return;
1154 }
1155
1156 var btn = e.target.closest( '[data-action="colstyles-reset"]' );
1157 if ( ! btn ) { return; }
1158
1159 // Clear dataset + activation flag and remove inline vars
1160 section_el.removeAttribute( 'data-colstyles-active' );
1161 section_el.removeAttribute( 'data-col_styles' );
1162 if ( section_el.dataset ) { delete section_el.dataset.col_styles; }
1163
1164 UI.WPBC_BFB_Column_Styles.apply( section_el, [] );
1165
1166 // Re-render fresh, not persisted
1167 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
1168
1169 if ( builder && builder.bus && Core.WPBC_BFB_Events ) {
1170 builder.bus.emit && builder.bus.emit( Core.WPBC_BFB_Events.STRUCTURE_CHANGE, { source : 'column_styles_reset', field : section_el } );
1171 }
1172 }
1173
1174 /**
1175 * Navigate one nested column-control tab list with the keyboard.
1176 *
1177 * @param {KeyboardEvent} e Inspector keydown event.
1178 * @returns {void}
1179 */
1180 function on_keydown( e ) {
1181 var control_tab = e.target.closest( '[data-column-control-tab-key]' );
1182 if ( ! control_tab || ! host.contains( control_tab ) ) { return; }
1183 if ( [ 'ArrowLeft', 'ArrowRight', 'Home', 'End' ].indexOf( e.key ) === -1 ) { return; }
1184
1185 var control_root = control_tab.closest( '[data-column-control-tabs]' );
1186 var control_tabs = Array.prototype.slice.call( control_root.querySelectorAll( '[data-column-control-tab-key]' ) );
1187 var active_index = control_tabs.indexOf( control_tab );
1188 if ( active_index < 0 ) { return; }
1189
1190 var next_index = active_index;
1191 if ( 'Home' === e.key ) {
1192 next_index = 0;
1193 } else if ( 'End' === e.key ) {
1194 next_index = control_tabs.length - 1;
1195 } else if ( 'ArrowRight' === e.key ) {
1196 next_index = ( active_index + 1 ) % control_tabs.length;
1197 } else {
1198 next_index = ( active_index - 1 + control_tabs.length ) % control_tabs.length;
1199 }
1200
1201 e.preventDefault();
1202 var next_tab_key = control_tabs[ next_index ].getAttribute( 'data-column-control-tab-key' ) || '';
1203 activate_column_control_tab( control_root, next_tab_key, true );
1204 remember_column_control_tab( control_root, next_tab_key );
1205 }
1206
1207 host.addEventListener( 'input', on_change, true );
1208 host.addEventListener( 'change', on_change, true );
1209 host.addEventListener( 'click', on_click, true );
1210 host.addEventListener( 'keydown', on_keydown, true );
1211
1212 // Initial apply (does NOT auto-activate).
1213 UI.WPBC_BFB_Column_Styles.apply( section_el, styles_arr );
1214
1215 // Provide cleanup to avoid leaks.
1216 host.__wpbc_cleanup = function () {
1217 try {
1218 host.removeEventListener( 'input', on_change, true );
1219 host.removeEventListener( 'change', on_change, true );
1220 host.removeEventListener( 'click', on_click, true );
1221 host.removeEventListener( 'keydown', on_keydown, true );
1222 } catch ( _e ) {}
1223 };
1224 },
1225
1226 /**
1227 * Refresh the mounted editor after columns count changes.
1228 *
1229 * @param {object} builder
1230 * @param {HTMLElement} section_el
1231 * @param {HTMLElement} inspector_root
1232 */
1233 refresh_for_section : function ( builder, section_el, inspector_root ) {
1234 var host = inspector_root && inspector_root.querySelector( '[data-bfb-slot="column_styles"]' );
1235 if ( ! host ) { return; }
1236 this.render_for_section( builder, section_el, host );
1237 }
1238 };
1239
1240 // Optional: register a factory slot for environments that use inspector factory.
1241 w.wpbc_bfb_inspector_factory_slots = w.wpbc_bfb_inspector_factory_slots || {};
1242 w.wpbc_bfb_inspector_factory_slots.column_styles = function ( host, opts ) {
1243 try {
1244 var builder = ( opts && opts.builder ) || w.wpbc_bfb || null;
1245 var section_el = ( opts && opts.el ) || ( builder && builder.get_selected_field && builder.get_selected_field() ) || null;
1246 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
1247 } catch ( e ) {
1248 w._wpbc && w._wpbc.dev && w._wpbc.dev.error && w._wpbc.dev.error( 'wpbc_bfb_inspector_factory_slots.column_styles', e );
1249 }
1250 };
1251
1252 })( window );
1253