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
← All changes | includes/page-form-builder/field-packs/section/_src/ui-column-styles.js +571 -118 11.2 → 11.9 View file →
@@ -1,4 +1,4 @@
1 1 /**
2 2 * == How to add a new CSS style ? ==
3 3 *
4 4 * TL;DR: add a key in COL_PROPS (JS) -> reference its CSS var (CSS) → add a control in the inspector template (HTML).
@@ -6,9 +6,9 @@
6 6 *
7 7 * -- 1) Register the style in this JS (COL_PROPS)
8 8 *
9 9 * // Length-like example:
10 - * pad: { var: '--wpbc-bfb-col-pad', def: '0px', normalize: 'len' }
10 + * padding: { var: '--wpbc-bfb-col-padding', def: '0px', normalize: 'len' }
11 11 *
12 12 * // Enum example:
13 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 14 *
@@ -17,20 +17,20 @@
17 17 * - If you need a new normalizer, add it to NORMALIZE and reference its name here.
18 18 *
19 19 * -- 2) Wire the CSS variable (defaults + activation)
20 20 *
21 - * File with defaults: ../includes/__css/client/form_fields/bfb_section__columns.css
21 + * Authoritative CSS: ../includes/page-form-builder/__css/bfb_fields.css
22 22 * The fallback in var(--name, <fallback>) MUST MATCH COL_PROPS[key].def.
23 23 *
24 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);
25 + * .wpbc_bfb__section__cols > .wpbc_bfb__section__col {
26 + * padding: var(--wpbc-bfb-col-padding, 0px);
27 27 * /* add other properties here using their vars *\/
28 28 * }
29 29 *
30 30 * /* Real columns (only when styles are activated) *\/
31 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);
32 + * padding: var(--wpbc-bfb-col-padding, 0px);
33 33 * }
34 34 *
35 35 * Where “default CSS settings” live:
36 36 * - The JS default: COL_PROPS[key].def (in this file) — used for parsing, preview, and activation checks.
@@ -42,9 +42,9 @@
42 42 * <!-- Simple input (works for 'len', 'id', and many enums with text inputs) -->
43 43 * <div class="inspector__row">
44 44 * <label class="inspector__label inspector__w_40">Padding</label>
45 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">
46 + * <input type="text" class="inspector__input" data-style-key="padding" data-col-idx="{{ i }}" placeholder="e.g., 8px or 0.5rem">
47 47 * </div>
48 48 * </div>
49 49 *
50 50 * The generic change handler will:
@@ -53,14 +53,14 @@
53 53 * - persist to data-col_styles (sparse JSON; defaults stripped),
54 54 * - toggle data-colstyles-active automatically,
55 55 * - and update preview + real columns.
56 56 *
57 - * -- 4) (Optional) Split value styles (like GAP number+unit)
57 + * -- 4) (Optional) Split length controls (number + unit)
58 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.
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 63 *
64 64 * -- 5) Activation & persistence (what happens under the hood)
65 65 *
66 66 * - The service compares saved values vs COL_PROPS defaults. If any non-default exists, it sets
@@ -71,9 +71,9 @@
71 71 * Checklist before you ship:
72 72 * [ ] COL_PROPS entry added with correct var name and def value
73 73 * [ ] CSS var() fallback matches the JS default exactly
74 74 * [ ] Inspector control present and uses data-style-key="<your key>"
75 - * [ ] (If split-value) on_change branch added, like for 'gap'
75 + * [ ] (If split-value) both controls use data-style-part and the registry uses normalize: 'len'
76 76 */
77 77
78 78
79 79 /**
@@ -85,10 +85,10 @@
85 85 *
86 86 * == File: /includes/page-form-builder/field-packs/section/_out/ui-column-styles.js
87 87 *
88 88 * @since 11.0.0
89 - * @modified 2025-09-16 11:25
90 - * @version 1.0.0
89 + * @modified 2026-09-19 16:30
90 + * @version 1.1.1
91 91 */
92 92 (function ( w ) {
93 93 'use strict';
94 94
@@ -111,22 +111,57 @@
111 111 *
112 112 * key — Short style key used in UI and persisted JSON.
113 113 * var — CSS variable name written onto the DOM node style.
114 114 * def — Default value if the style is unset/empty.
115 - * normalize — Normalizer id ('id' passthrough, 'len' for length units).
115 + * normalize — Normalizer id or enum definition (`id`, `len`, `max_len`, or allowlisted values).
116 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 - }
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 + }
126 153 // Example additions:
127 - // pad : { var: '--wpbc-bfb-col-pad', def: '0px', normalize: 'len' }
128 - };
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' ];
129 164
130 165 /**
131 166 * Normalize a "length-like" value (e.g., "8" → "8px").
132 167 * Accepts px/rem/em/%; expand the regex if you allow more units.
@@ -133,15 +168,38 @@
133 168 *
134 169 * @param {string|number} v
135 170 * @returns {string} normalized value (always non-empty)
136 171 */
137 - function norm_len( v ) {
172 + function norm_len( v ) {
138 173 var sv = String( v || '' ).trim();
139 174 if ( ! sv ) { return '0px'; }
140 175 if ( /^\d+(\.\d+)?$/.test( sv ) ) { return sv + 'px'; } // number -> px.
141 176 if ( /^\d+(\.\d+)?(px|rem|em|%)$/.test( sv ) ) { return sv; } // allowed units.
142 - return '0px';
143 - }
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 + }
144 202
145 203 /**
146 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'] } }
147 205 *
@@ -157,13 +215,14 @@
157 215 /**
158 216 * Normalizer registry. Extend to add custom validators/transforms
159 217 * (e.g., enums, numbers with ranges, etc.).
160 218 */
161 - var NORMALIZE = {
162 - id : v => String( v || '' ),
163 - len : norm_len,
164 - enum: (v, values) => norm_enum( v, values )
165 - };
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 + };
166 225
167 226 /**
168 227 * Check whether a style key is supported by COL_PROPS.
169 228 *
@@ -197,12 +256,169 @@
197 256 * Build a plain object containing defaults for all supported style keys.
198 257 *
199 258 * @returns {Record<string, string>}
200 259 */
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 - }
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 + }
205 421
206 422 /**
207 423 * Apply all CSS variables (from COL_PROPS) onto a node based on a style object.
208 424 * Missing/empty values fall back to defaults.
@@ -211,21 +427,21 @@
211 427 * @param {Record<string, string>} style_obj
212 428 */
213 429 function set_vars( node, style_obj ) {
214 430 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 - }
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 + }
221 437
222 438 function set_vars_sparse(node, style_obj) {
223 439 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]));
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] ));
228 444 } else {
229 445 // important: remove var instead of writing a default
230 446 node.style.removeProperty(cssVar);
231 447 }
@@ -272,11 +488,22 @@
272 488
273 489 // ------------------------------------------------------------------------------------------------
274 490 // Service: Column Styles
275 491 // ------------------------------------------------------------------------------------------------
276 - UI.WPBC_BFB_Column_Styles = {
277 -
278 - /**
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 + /**
279 506 * Highlight a specific column in both the live canvas and the mini preview,
280 507 * and store the active index on the section.
281 508 *
282 509 * @param {HTMLElement} section_el
@@ -334,9 +561,9 @@
334 561 /**
335 562 * Parse JSON string to array of per-column style objects.
336 563 *
337 564 * @param {string} s
338 - * @returns {Array<{dir:string,wrap:string,jc:string,ai:string,gap:string}>}
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}>}
339 566 */
340 567 parse_col_styles : function ( s ) {
341 568 if ( ! s ) { return []; }
342 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; } } )() );
@@ -446,16 +673,28 @@
446 673 * @param {object} builder
447 674 * @param {HTMLElement} section_el
448 675 * @param {HTMLElement} host
449 676 */
450 - render_for_section : function ( builder, section_el, host ) {
451 - if ( ! host || ! section_el ) { return; }
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;
452 696
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 697 // Cleanup previous mount and clear.
459 698 if ( host.__wpbc_cleanup ) {
460 699 try { host.__wpbc_cleanup(); } catch ( _e ) {}
461 700 host.__wpbc_cleanup = null;
@@ -467,34 +706,53 @@
467 706
468 707 var col_count = dom_cols( section_el );
469 708 var raw_json = section_el.getAttribute( 'data-col_styles' ) || ( section_el.dataset ? ( section_el.dataset.col_styles || '' ) : '' );
470 709
471 - var saved_arr = UI.WPBC_BFB_Column_Styles.parse_col_styles( raw_json );
472 - var styles_arr = [];
710 + var saved_arr = UI.WPBC_BFB_Column_Styles.parse_col_styles( raw_json );
711 + var styles_arr = [];
473 712
474 713 // Normalize length to current columns (UI-only defaults do NOT auto-activate).
475 714 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 - };
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 + }
488 739 styles_arr[i] = full;
489 740 }
490 741 styles_arr.length = col_count; // clamp.
491 742
492 - host.innerHTML = tpl( {
493 - cols : col_count,
494 - styles: styles_arr,
495 - active: UI.WPBC_BFB_Column_Styles.is_active( section_el )
496 - } );
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 + }
497 755
498 756 if ( window.wpbc_ui_tabs && host ) {
499 757 window.wpbc_ui_tabs.init_on( host );
500 758
@@ -532,13 +790,18 @@
532 790 UI.WPBC_BFB_Column_Styles.set_selected_col_flag( section_el, __active_key_now );
533 791 }
534 792
535 793 // 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 ) {}
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 + }
541 804
542 805 // Set initial state of ICONS (including defaults) is correct.
543 806 sync_axis_rotation_all();
544 807
@@ -567,10 +830,11 @@
567 830 var row = {};
568 831 for ( var k in def ) {
569 832 if ( Object.prototype.hasOwnProperty.call( def, k ) ) {
570 833 var v = (s[k] == null) ? '' : String( s[k] );
571 - if ( v && v !== String( def[k] ) ) {
572 - row[k] = v; // only keep meaningful overrides.
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.
573 837 }
574 838 }
575 839 }
576 840 out.push( row );
@@ -605,9 +869,9 @@
605 869 /**
606 870 * Apply rotation class to *all* columns, using the effective `dir` value
607 871 * (saved value or default from COL_PROPS).
608 872 */
609 - function sync_axis_rotation_all() {
873 + function sync_axis_rotation_all() {
610 874 var def = get_defaults_obj(); // includes def.dir (which is 'column' in your code).
611 875 for ( var i = 0; i < styles_arr.length; i++ ) {
612 876 var dir_val = ( styles_arr[i] && styles_arr[i].dir ) ? String( styles_arr[i].dir ) : String( def.dir );
613 877 var enable = ( dir_val === 'column' );
@@ -612,9 +876,98 @@
612 876 var dir_val = ( styles_arr[i] && styles_arr[i].dir ) ? String( styles_arr[i].dir ) : String( def.dir );
613 877 var enable = ( dir_val === 'column' );
614 878 toggle_axis_rotation_for_col( i, enable );
615 879 }
616 - }
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 + }
617 970
618 971 // Delay (ms) for deferred UI updates after changing layout combo.
619 972 var rerender_delay_ms = 420;
620 973
@@ -649,14 +1002,14 @@
649 1002 }, delay );
650 1003 }
651 1004
652 1005
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 {
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 {
659 1012 section_el.removeAttribute( 'data-colstyles-active' );
660 1013 }
661 1014
662 1015 // Normalize length to current number of columns (keeps attribute tidy).
@@ -682,9 +1035,9 @@
682 1035 }
683 1036 }
684 1037
685 1038
686 - function on_change( e ) {
1039 + function on_change( e ) {
687 1040 var t = e.target;
688 1041 // Radios fire both 'input' and 'change' in most browsers.
689 1042 if (t && t.type === 'radio' && e.type === 'input') return;
690 1043
@@ -706,18 +1059,24 @@
706 1059 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
707 1060 return;
708 1061 }
709 1062
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;
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;
720 1079 }
721 1080
722 1081 // dir: commit now, rotate immediately for snappy feedback, still re-render after delay.
723 1082 if ( key === 'dir' ) {
@@ -727,16 +1086,75 @@
727 1086 schedule_rerender( idx, styles_arr[idx].dir, { rotate_now: true, delay: rerender_delay_ms } );
728 1087 return;
729 1088 }
730 1089
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"]' );
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"]' );
739 1157 if ( ! btn ) { return; }
740 1158
741 1159 // Clear dataset + activation flag and remove inline vars
742 1160 section_el.removeAttribute( 'data-colstyles-active' );
@@ -749,14 +1167,48 @@
749 1167 UI.wpbc_bfb_column_styles.render_for_section( builder, section_el, host );
750 1168
751 1169 if ( builder && builder.bus && Core.WPBC_BFB_Events ) {
752 1170 builder.bus.emit && builder.bus.emit( Core.WPBC_BFB_Events.STRUCTURE_CHANGE, { source : 'column_styles_reset', field : section_el } );
753 - }
754 - }
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 + }
755 1206
756 1207 host.addEventListener( 'input', on_change, true );
757 - host.addEventListener( 'change', on_change, true );
758 - host.addEventListener( 'click', on_click, true );
1208 + host.addEventListener( 'change', on_change, true );
1209 + host.addEventListener( 'click', on_click, true );
1210 + host.addEventListener( 'keydown', on_keydown, true );
759 1211
760 1212 // Initial apply (does NOT auto-activate).
761 1213 UI.WPBC_BFB_Column_Styles.apply( section_el, styles_arr );
762 1214
@@ -763,10 +1215,11 @@
763 1215 // Provide cleanup to avoid leaks.
764 1216 host.__wpbc_cleanup = function () {
765 1217 try {
766 1218 host.removeEventListener( 'input', on_change, true );
767 - host.removeEventListener( 'change', on_change, true );
768 - host.removeEventListener( 'click', on_click, true );
1219 + host.removeEventListener( 'change', on_change, true );
1220 + host.removeEventListener( 'click', on_click, true );
1221 + host.removeEventListener( 'keydown', on_keydown, true );
769 1222 } catch ( _e ) {}
770 1223 };
771 1224 },
772 1225