| 1 |
// --------------------------------------------------------------------------------------------------------------------- |
| 2 |
// == File /includes/page-form-builder/_out/core/bfb-fields.js == | 2025-09-10 15:47 |
| 3 |
// --------------------------------------------------------------------------------------------------------------------- |
| 4 |
(function ( w ) { |
| 5 |
'use strict'; |
| 6 |
|
| 7 |
// Single global namespace (idempotent & load-order safe). |
| 8 |
const Core = ( w.WPBC_BFB_Core = w.WPBC_BFB_Core || {} ); |
| 9 |
const UI = ( Core.UI = Core.UI || {} ); |
| 10 |
|
| 11 |
/** |
| 12 |
* Base class for field renderers (static-only contract). |
| 13 |
* ================================================================================================================ |
| 14 |
* Contract exposed to the builder (static methods on the CLASS itself): |
| 15 |
* - render(el, data, ctx) // REQUIRED |
| 16 |
* - on_field_drop(data, el, meta) // OPTIONAL (default provided) |
| 17 |
* |
| 18 |
* Helpers for subclasses: |
| 19 |
* - get_defaults() -> per-field defaults (MUST override in subclass to set type/label) |
| 20 |
* - normalize_data(d) -> shallow merge with defaults |
| 21 |
* - get_template(id) -> per-id cached wp.template compiler |
| 22 |
* |
| 23 |
* Subclass usage: |
| 24 |
* class WPBC_BFB_Field_Text extends Core.WPBC_BFB_Field_Base { static get_defaults(){ ... } } |
| 25 |
* WPBC_BFB_Field_Text.template_id = 'wpbc-bfb-field-text'; |
| 26 |
* ================================================================================================================ |
| 27 |
*/ |
| 28 |
Core.WPBC_BFB_Field_Base = class { |
| 29 |
|
| 30 |
/** |
| 31 |
* Default field data (generic baseline). |
| 32 |
* Subclasses MUST override to provide { type, label } appropriate for the field. |
| 33 |
* @returns {Object} |
| 34 |
*/ |
| 35 |
static get_defaults() { |
| 36 |
return { |
| 37 |
type : 'field', |
| 38 |
label : 'Field', |
| 39 |
name : 'field', |
| 40 |
html_id : '', |
| 41 |
placeholder : '', |
| 42 |
required : false, |
| 43 |
minlength : '', |
| 44 |
maxlength : '', |
| 45 |
pattern : '', |
| 46 |
cssclass : '', |
| 47 |
help : '' |
| 48 |
}; |
| 49 |
} |
| 50 |
|
| 51 |
/** |
| 52 |
* Shallow-merge incoming data with defaults. |
| 53 |
* @param {Object} data |
| 54 |
* @returns {Object} |
| 55 |
*/ |
| 56 |
static normalize_data( data ) { |
| 57 |
var d = data || {}; |
| 58 |
var defaults = this.get_defaults(); |
| 59 |
var out = {}; |
| 60 |
var k; |
| 61 |
|
| 62 |
for ( k in defaults ) { |
| 63 |
if ( Object.prototype.hasOwnProperty.call( defaults, k ) ) { |
| 64 |
out[k] = defaults[k]; |
| 65 |
} |
| 66 |
} |
| 67 |
for ( k in d ) { |
| 68 |
if ( Object.prototype.hasOwnProperty.call( d, k ) ) { |
| 69 |
out[k] = d[k]; |
| 70 |
} |
| 71 |
} |
| 72 |
return out; |
| 73 |
} |
| 74 |
|
| 75 |
/** |
| 76 |
* Compile and cache a wp.template by id (per-id cache). |
| 77 |
* @param {string} template_id |
| 78 |
* @returns {Function|null} |
| 79 |
*/ |
| 80 |
static get_template(template_id) { |
| 81 |
|
| 82 |
// Accept either "wpbc-bfb-field-text" or "tmpl-wpbc-bfb-field-text". |
| 83 |
if ( ! template_id || ! window.wp || ! wp.template ) { |
| 84 |
return null; |
| 85 |
} |
| 86 |
const domId = template_id.startsWith( 'tmpl-' ) ? template_id : ('tmpl-' + template_id); |
| 87 |
if ( ! document.getElementById( domId ) ) { |
| 88 |
return null; |
| 89 |
} |
| 90 |
|
| 91 |
if ( ! Core.__bfb_tpl_cache_map ) { |
| 92 |
Core.__bfb_tpl_cache_map = {}; |
| 93 |
} |
| 94 |
|
| 95 |
// Normalize id for the compiler & cache. // wp.template expects id WITHOUT the "tmpl-" prefix ! |
| 96 |
const key = template_id.replace( /^tmpl-/, '' ); |
| 97 |
if ( Core.__bfb_tpl_cache_map[key] ) { |
| 98 |
return Core.__bfb_tpl_cache_map[key]; |
| 99 |
} |
| 100 |
|
| 101 |
const compiler = wp.template( key ); // <-- normalized id here |
| 102 |
if ( compiler ) { |
| 103 |
Core.__bfb_tpl_cache_map[key] = compiler; |
| 104 |
} |
| 105 |
|
| 106 |
return compiler; |
| 107 |
} |
| 108 |
|
| 109 |
/** |
| 110 |
* REQUIRED: render preview into host element (full redraw; idempotent). |
| 111 |
* Subclasses should set static `template_id` to a valid wp.template id. |
| 112 |
* @param {HTMLElement} el |
| 113 |
* @param {Object} data |
| 114 |
* @param {{mode?:string,builder?:any,tpl?:Function,sanit?:any}} ctx |
| 115 |
* @returns {void} |
| 116 |
*/ |
| 117 |
static render( el, data, ctx ) { |
| 118 |
if ( ! el ) { |
| 119 |
return; |
| 120 |
} |
| 121 |
|
| 122 |
var compile = this.get_template( this.template_id ); |
| 123 |
var d = this.normalize_data( data ); |
| 124 |
|
| 125 |
var s = (ctx && ctx.sanit) ? ctx.sanit : Core.WPBC_BFB_Sanitize; |
| 126 |
|
| 127 |
// Sanitize critical attributes before templating. |
| 128 |
if ( s ) { |
| 129 |
d.html_id = d.html_id ? s.sanitize_html_id( String( d.html_id ) ) : ''; |
| 130 |
d.name = s.sanitize_html_name( String( d.name || d.id || 'field' ) ); |
| 131 |
} else { |
| 132 |
d.html_id = d.html_id ? String( d.html_id ) : ''; |
| 133 |
d.name = String( d.name || d.id || 'field' ); |
| 134 |
} |
| 135 |
|
| 136 |
// Fall back to generic preview if template not available. |
| 137 |
if ( compile ) { |
| 138 |
el.innerHTML = compile( d ); |
| 139 |
|
| 140 |
// After render, set attribute values via DOM so quotes/newlines are handled correctly. |
| 141 |
const input = el.querySelector( 'input, textarea, select' ); |
| 142 |
if ( input ) { |
| 143 |
if ( d.placeholder != null ) input.setAttribute( 'placeholder', String( d.placeholder ) ); |
| 144 |
if ( d.title != null ) input.setAttribute( 'title', String( d.title ) ); |
| 145 |
} |
| 146 |
|
| 147 |
} else { |
| 148 |
el.innerHTML = Core.WPBC_Form_Builder_Helper.render_field_inner_html( d ); |
| 149 |
} |
| 150 |
|
| 151 |
el.dataset.type = d.type || 'field'; |
| 152 |
el.setAttribute( 'data-label', (d.label != null ? String( d.label ) : '') ); // allow "". |
| 153 |
} |
| 154 |
|
| 155 |
|
| 156 |
/** |
| 157 |
* OPTIONAL hook executed after field is dropped/loaded/preview. |
| 158 |
* Default extended: |
| 159 |
* - On first drop: stamp default label (existing behavior) and mark field as "fresh" for auto-name. |
| 160 |
* - On load: mark as loaded so later label edits do not rename the saved name. |
| 161 |
*/ |
| 162 |
static on_field_drop(data, el, meta) { |
| 163 |
|
| 164 |
const context = (meta && meta.context) ? String( meta.context ) : ''; |
| 165 |
|
| 166 |
// ----------------------------------------------------------------------------------------- |
| 167 |
// NEW: Seed default "help" (and keep it in Structure) for all field packs that define it. |
| 168 |
// This fixes the mismatch where: |
| 169 |
// - UI shows default help via normalize_data() / templates |
| 170 |
// - but get_structure() / exporters see `help` as undefined/empty. |
| 171 |
// |
| 172 |
// Behavior: |
| 173 |
// - Runs ONLY on initial drop (context === 'drop'). |
| 174 |
// - If get_defaults() exposes a non-empty "help", and data.help is |
| 175 |
// missing / null / empty string -> we persist the default into `data` |
| 176 |
// and notify Structure so exports see it. |
| 177 |
// - On "load" we do nothing, so existing forms where user *cleared* |
| 178 |
// help will not be overridden. |
| 179 |
// ----------------------------------------------------------------------------------------- |
| 180 |
if ( context === 'drop' && data ) { |
| 181 |
try { |
| 182 |
const defs = (typeof this.get_defaults === 'function') ? this.get_defaults() : null; |
| 183 |
if ( defs && Object.prototype.hasOwnProperty.call( defs, 'help' ) ) { |
| 184 |
const current = Object.prototype.hasOwnProperty.call( data, 'help' ) ? data.help : undefined; |
| 185 |
const hasValue = (current !== undefined && current !== null && String( current ) !== ''); |
| 186 |
const defaultVal = defs.help; |
| 187 |
|
| 188 |
if ( ! hasValue && defaultVal != null && String( defaultVal ) !== '' ) { |
| 189 |
// 1) persist into data object (used by Structure). |
| 190 |
data.help = defaultVal; |
| 191 |
|
| 192 |
// 2) mirror into dataset (for any DOM-based consumers). |
| 193 |
if ( el ) { |
| 194 |
el.dataset.help = String( defaultVal ); |
| 195 |
|
| 196 |
// 3) notify Structure / listeners (if available). |
| 197 |
try { |
| 198 |
Core.Structure?.update_field_prop?.( el, 'help', defaultVal ); |
| 199 |
el.dispatchEvent( |
| 200 |
new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail : { key: 'help', value: defaultVal } } ) |
| 201 |
); |
| 202 |
} catch ( _inner ) {} |
| 203 |
} |
| 204 |
} |
| 205 |
} |
| 206 |
} catch ( _e ) {} |
| 207 |
} |
| 208 |
// ----------------------------------------------------------------------------------------- |
| 209 |
|
| 210 |
if ( context === 'drop' && !Object.prototype.hasOwnProperty.call( data, 'label' ) ) { |
| 211 |
const defs = this.get_defaults(); |
| 212 |
data.label = defs.label || 'Field'; |
| 213 |
el.setAttribute( 'data-label', data.label ); |
| 214 |
} |
| 215 |
// Mark provenance flags. |
| 216 |
if ( context === 'drop' ) { |
| 217 |
el.dataset.fresh = '1'; // can auto-name on first label edit. |
| 218 |
el.dataset.autoname = '1'; |
| 219 |
el.dataset.was_loaded = '0'; |
| 220 |
// Seed a provisional unique name immediately. |
| 221 |
try { |
| 222 |
const b = meta?.builder; |
| 223 |
if ( b?.id && (!el.hasAttribute( 'data-name' ) || !el.getAttribute( 'data-name' )) ) { |
| 224 |
const S = Core.WPBC_BFB_Sanitize; |
| 225 |
const base = S.sanitize_html_name( el.getAttribute( 'data-id' ) || data?.id || data?.type || 'field' ); |
| 226 |
const uniq = b.id.ensure_unique_field_name( base, el ); |
| 227 |
el.setAttribute( 'data-name', uniq ); |
| 228 |
el.dataset.name_user_touched = '0'; |
| 229 |
} |
| 230 |
} catch ( _ ) {} |
| 231 |
|
| 232 |
} else if ( context === 'load' ) { |
| 233 |
el.dataset.fresh = '0'; |
| 234 |
el.dataset.autoname = '0'; |
| 235 |
el.dataset.was_loaded = '1'; // never rename names for loaded fields. |
| 236 |
} |
| 237 |
} |
| 238 |
|
| 239 |
// --- Auto Rename "Fresh" field, on entering the new Label --- |
| 240 |
|
| 241 |
/** |
| 242 |
* Create a conservative field "name" from a human label. |
| 243 |
* Uses the same constraints as sanitize_html_name (letters/digits/_- and leading letter). |
| 244 |
*/ |
| 245 |
static name_from_label(label) { |
| 246 |
const s = Core.WPBC_BFB_Sanitize.sanitize_html_name( String( label ?? '' ) ); |
| 247 |
return s.toLowerCase() || 'field'; |
| 248 |
} |
| 249 |
|
| 250 |
/** |
| 251 |
* Auto-fill data-name from label ONLY for freshly dropped fields that were not edited yet. |
| 252 |
* - Never runs for sections. |
| 253 |
* - Never runs for loaded/existing fields. |
| 254 |
* - Stops as soon as user edits the Name manually. |
| 255 |
* |
| 256 |
* @param {WPBC_Form_Builder} builder |
| 257 |
* @param {HTMLElement} el - .wpbc_bfb__field element |
| 258 |
* @param {string} labelVal |
| 259 |
*/ |
| 260 |
static maybe_autoname_from_label(builder, el, labelVal) { |
| 261 |
if ( !builder || !el ) return; |
| 262 |
if ( el.classList.contains( 'wpbc_bfb__section' ) ) return; |
| 263 |
|
| 264 |
const allowAuto = el.dataset.autoname === '1'; |
| 265 |
|
| 266 |
const userTouched = el.dataset.name_user_touched === '1'; |
| 267 |
const isLoaded = el.dataset.was_loaded === '1'; |
| 268 |
|
| 269 |
if ( !allowAuto || userTouched || isLoaded ) return; |
| 270 |
|
| 271 |
// Only override placeholder-y names |
| 272 |
const S = Core.WPBC_BFB_Sanitize; |
| 273 |
|
| 274 |
const base = this.name_from_label( labelVal ); |
| 275 |
const unique = builder.id.ensure_unique_field_name( base, el ); |
| 276 |
el.setAttribute( 'data-name', unique ); |
| 277 |
|
| 278 |
const ins = document.getElementById( 'wpbc_bfb__inspector' ); |
| 279 |
const nameCtrl = ins?.querySelector( '[data-inspector-key="name"]' ); |
| 280 |
if ( nameCtrl && 'value' in nameCtrl && nameCtrl.value !== unique ) nameCtrl.value = unique; |
| 281 |
} |
| 282 |
|
| 283 |
|
| 284 |
}; |
| 285 |
|
| 286 |
/** |
| 287 |
* Select_Base (shared base for select-like packs) |
| 288 |
* |
| 289 |
* @type {Core.WPBC_BFB_Select_Base} |
| 290 |
*/ |
| 291 |
Core.WPBC_BFB_Select_Base = class extends Core.WPBC_BFB_Field_Base { |
| 292 |
|
| 293 |
static template_id = null; // main preview template id |
| 294 |
static option_row_template_id = 'wpbc-bfb-inspector-select-option-row'; // row tpl id |
| 295 |
static kind = 'select'; |
| 296 |
static __root_wired = false; |
| 297 |
static __root_node = null; |
| 298 |
|
| 299 |
// Single source of selectors used by the inspector UI. |
| 300 |
static ui = { |
| 301 |
list : '.wpbc_bfb__options_list', |
| 302 |
holder : '.wpbc_bfb__options_state[data-inspector-key="options"]', |
| 303 |
row : '.wpbc_bfb__options_row', |
| 304 |
label : '.wpbc_bfb__opt-label', |
| 305 |
value : '.wpbc_bfb__opt-value', |
| 306 |
toggle : '.wpbc_bfb__opt-selected-chk', |
| 307 |
add_btn: '.js-add-option', |
| 308 |
|
| 309 |
drag_handle : '.wpbc_bfb__drag-handle', |
| 310 |
multiple_chk : '.js-opt-multiple[data-inspector-key="multiple"]', |
| 311 |
default_text : '.js-default-value[data-inspector-key="default_value"]', |
| 312 |
placeholder_input: '.js-placeholder[data-inspector-key="placeholder"]', |
| 313 |
placeholder_note : '.js-placeholder-note', |
| 314 |
size_input : '.inspector__input[data-inspector-key="size"]', |
| 315 |
|
| 316 |
// Dropdown menu integration. |
| 317 |
menu_root : '.wpbc_ui_el__dropdown', |
| 318 |
menu_toggle: '[data-toggle="wpbc_dropdown"]', |
| 319 |
menu_action: '.ul_dropdown_menu_li_action[data-action]', |
| 320 |
// Value-differs toggle. |
| 321 |
value_differs_chk: '.js-value-differs[data-inspector-key="value_differs"]', |
| 322 |
}; |
| 323 |
|
| 324 |
/** |
| 325 |
* Build option value from label. |
| 326 |
* - If `differs === true` -> generate token (slug-like machine value). |
| 327 |
* - If `differs === false` -> keep human text; escape only dangerous chars. |
| 328 |
* @param {string} label |
| 329 |
* @param {boolean} differs |
| 330 |
* @returns {string} |
| 331 |
*/ |
| 332 |
static build_value_from_label(label, differs) { |
| 333 |
const S = Core.WPBC_BFB_Sanitize; |
| 334 |
if ( differs ) { |
| 335 |
return (S && typeof S.to_token === 'function') |
| 336 |
? S.to_token( String( label || '' ) ) |
| 337 |
: String( label || '' ).trim().toLowerCase().replace( /\s+/g, '_' ).replace( /[^\w-]/g, '' ); |
| 338 |
} |
| 339 |
// single-input mode: keep human text; template will escape safely. |
| 340 |
return String( label == null ? '' : label ); |
| 341 |
} |
| 342 |
|
| 343 |
/** |
| 344 |
* Is the “value differs from label” toggle enabled? |
| 345 |
* @param {HTMLElement} panel |
| 346 |
* @returns {boolean} |
| 347 |
*/ |
| 348 |
static is_value_differs_enabled(panel) { |
| 349 |
const chk = panel?.querySelector( this.ui.value_differs_chk ); |
| 350 |
return !!(chk && chk.checked); |
| 351 |
} |
| 352 |
|
| 353 |
/** |
| 354 |
* Ensure visibility/enabled state of Value inputs based on the toggle. |
| 355 |
* When disabled -> hide Value inputs and keep them mirrored from Label. |
| 356 |
* @param {HTMLElement} panel |
| 357 |
* @returns {void} |
| 358 |
*/ |
| 359 |
static sync_value_inputs_visibility(panel) { |
| 360 |
const differs = this.is_value_differs_enabled( panel ); |
| 361 |
const rows = panel?.querySelectorAll( this.ui.row ) || []; |
| 362 |
|
| 363 |
for ( let i = 0; i < rows.length; i++ ) { |
| 364 |
const r = rows[i]; |
| 365 |
const lbl_in = r.querySelector( this.ui.label ); |
| 366 |
const val_in = r.querySelector( this.ui.value ); |
| 367 |
if ( !val_in ) continue; |
| 368 |
|
| 369 |
if ( differs ) { |
| 370 |
// Re-enable & show value input |
| 371 |
val_in.removeAttribute( 'disabled' ); |
| 372 |
val_in.style.display = ''; |
| 373 |
|
| 374 |
// If we have a cached custom value and the row wasn't edited while OFF, restore it |
| 375 |
const hasCache = !!val_in.dataset.cached_value; |
| 376 |
const userEdited = r.dataset.value_user_touched === '1'; |
| 377 |
|
| 378 |
if ( hasCache && !userEdited ) { |
| 379 |
val_in.value = val_in.dataset.cached_value; |
| 380 |
} else if ( !hasCache ) { |
| 381 |
// No cache: if value is just a mirrored label, offer a tokenized default |
| 382 |
const lbl = lbl_in ? lbl_in.value : ''; |
| 383 |
const mirrored = this.build_value_from_label( lbl, /*differs=*/false ); |
| 384 |
if ( val_in.value === mirrored ) { |
| 385 |
val_in.value = this.build_value_from_label( lbl, /*differs=*/true ); |
| 386 |
} |
| 387 |
} |
| 388 |
} else { |
| 389 |
// ON -> OFF: cache once, then mirror |
| 390 |
if ( !val_in.dataset.cached_value ) { |
| 391 |
val_in.dataset.cached_value = val_in.value || ''; |
| 392 |
} |
| 393 |
const lbl = lbl_in ? lbl_in.value : ''; |
| 394 |
val_in.value = this.build_value_from_label( lbl, /*differs=*/false ); |
| 395 |
|
| 396 |
val_in.setAttribute( 'disabled', 'disabled' ); |
| 397 |
val_in.style.display = 'none'; |
| 398 |
// NOTE: do NOT mark as user_touched here |
| 399 |
} |
| 400 |
} |
| 401 |
} |
| 402 |
|
| 403 |
|
| 404 |
/** |
| 405 |
* Return whether this row’s value has been edited by user. |
| 406 |
* @param {HTMLElement} row |
| 407 |
* @returns {boolean} |
| 408 |
*/ |
| 409 |
static is_row_value_user_touched(row) { |
| 410 |
return row?.dataset?.value_user_touched === '1'; |
| 411 |
} |
| 412 |
|
| 413 |
/** |
| 414 |
* Mark this row’s value as edited by user. |
| 415 |
* @param {HTMLElement} row |
| 416 |
*/ |
| 417 |
static mark_row_value_user_touched(row) { |
| 418 |
if ( row ) row.dataset.value_user_touched = '1'; |
| 419 |
} |
| 420 |
|
| 421 |
/** |
| 422 |
* Initialize “freshness” flags on a row (value untouched). |
| 423 |
* Call on creation/append of rows. |
| 424 |
* @param {HTMLElement} row |
| 425 |
*/ |
| 426 |
static init_row_fresh_flags(row) { |
| 427 |
if ( row ) { |
| 428 |
if ( !row.dataset.value_user_touched ) { |
| 429 |
row.dataset.value_user_touched = '0'; |
| 430 |
} |
| 431 |
} |
| 432 |
} |
| 433 |
|
| 434 |
// ---- defaults (packs can override) ---- |
| 435 |
static get_defaults() { |
| 436 |
return { |
| 437 |
type : this.kind, |
| 438 |
label : 'Select', |
| 439 |
name : '', |
| 440 |
html_id : '', |
| 441 |
placeholder : '--- Select ---', |
| 442 |
required : false, |
| 443 |
multiple : false, |
| 444 |
size : null, |
| 445 |
cssclass : '', |
| 446 |
help : '', |
| 447 |
default_value: '', |
| 448 |
options : [ |
| 449 |
{ label: 'Option 1', value: 'Option 1', selected: false }, |
| 450 |
{ label: 'Option 2', value: 'Option 2', selected: false }, |
| 451 |
{ label: 'Option 3', value: 'Option 3', selected: false }, |
| 452 |
{ label: 'Option 4', value: 'Option 4', selected: false } |
| 453 |
], |
| 454 |
min_width : '240px' |
| 455 |
}; |
| 456 |
} |
| 457 |
|
| 458 |
// ---- preview render (idempotent) ---- |
| 459 |
static render(el, data, ctx) { |
| 460 |
if ( !el ) return; |
| 461 |
|
| 462 |
const d = this.normalize_data( data ); |
| 463 |
|
| 464 |
if ( d.min_width != null ) { |
| 465 |
el.dataset.min_width = String( d.min_width ); |
| 466 |
try { |
| 467 |
el.style.setProperty( '--wpbc-col-min', String( d.min_width ) ); |
| 468 |
} catch ( _ ) { |
| 469 |
} |
| 470 |
} |
| 471 |
if ( d.html_id != null ) el.dataset.html_id = String( d.html_id || '' ); |
| 472 |
if ( d.cssclass != null ) el.dataset.cssclass = String( d.cssclass || '' ); |
| 473 |
if ( d.placeholder != null ) el.dataset.placeholder = String( d.placeholder || '' ); |
| 474 |
|
| 475 |
const tpl = this.get_template( this.template_id ); |
| 476 |
if ( typeof tpl !== 'function' ) { |
| 477 |
el.innerHTML = '<div class="wpbc_bfb__error" role="alert">Template not found: ' + this.template_id + '.</div>'; |
| 478 |
return; |
| 479 |
} |
| 480 |
|
| 481 |
try { |
| 482 |
el.innerHTML = tpl( d ); |
| 483 |
} catch ( e ) { |
| 484 |
window._wpbc?.dev?.error?.( 'Select_Base.render', e ); |
| 485 |
el.innerHTML = '<div class="wpbc_bfb__error" role="alert">Error rendering field preview.</div>'; |
| 486 |
return; |
| 487 |
} |
| 488 |
|
| 489 |
el.dataset.type = d.type || this.kind; |
| 490 |
el.setAttribute( 'data-label', (d.label != null ? String( d.label ) : '') ); |
| 491 |
|
| 492 |
try { |
| 493 |
Core.UI?.WPBC_BFB_Overlay?.ensure?.( ctx?.builder, el ); |
| 494 |
} catch ( _ ) { |
| 495 |
} |
| 496 |
|
| 497 |
if ( !el.dataset.options && Array.isArray( d.options ) && d.options.length ) { |
| 498 |
try { |
| 499 |
el.dataset.options = JSON.stringify( d.options ); |
| 500 |
} catch ( _ ) { |
| 501 |
} |
| 502 |
} |
| 503 |
} |
| 504 |
|
| 505 |
// ---- drop seeding (options + placeholder) ---- |
| 506 |
static on_field_drop(data, el, meta) { |
| 507 |
try { |
| 508 |
super.on_field_drop?.( data, el, meta ); |
| 509 |
} catch ( _ ) { |
| 510 |
} |
| 511 |
|
| 512 |
const is_drop = (meta && meta.context === 'drop'); |
| 513 |
|
| 514 |
if ( is_drop ) { |
| 515 |
if ( !Array.isArray( data.options ) || !data.options.length ) { |
| 516 |
const opts = (this.get_defaults().options || []).map( (o) => ({ |
| 517 |
label : o.label, |
| 518 |
value : o.value, |
| 519 |
selected: !!o.selected |
| 520 |
}) ); |
| 521 |
data.options = opts; |
| 522 |
try { |
| 523 |
el.dataset.options = JSON.stringify( opts ); |
| 524 |
el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, |
| 525 |
detail : { |
| 526 |
key : 'options', |
| 527 |
value: opts |
| 528 |
} |
| 529 |
} ) ); |
| 530 |
Core.Structure?.update_field_prop?.( el, 'options', opts ); |
| 531 |
} catch ( _ ) { |
| 532 |
} |
| 533 |
} |
| 534 |
|
| 535 |
const ph = (data.placeholder ?? '').toString().trim(); |
| 536 |
if ( !ph ) { |
| 537 |
const dflt = this.get_defaults().placeholder || '--- Select ---'; |
| 538 |
data.placeholder = dflt; |
| 539 |
try { |
| 540 |
el.dataset.placeholder = String( dflt ); |
| 541 |
el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, |
| 542 |
detail : { |
| 543 |
key : 'placeholder', |
| 544 |
value: dflt |
| 545 |
} |
| 546 |
} ) ); |
| 547 |
Core.Structure?.update_field_prop?.( el, 'placeholder', dflt ); |
| 548 |
} catch ( _ ) { |
| 549 |
} |
| 550 |
} |
| 551 |
} |
| 552 |
} |
| 553 |
|
| 554 |
// ============================== |
| 555 |
// Inspector helpers (snake_case) |
| 556 |
// ============================== |
| 557 |
static get_panel_root(el) { |
| 558 |
return el?.closest?.( '.wpbc_bfb__inspector__body' ) || el?.closest?.( '.wpbc_bfb__inspector' ) || null; |
| 559 |
} |
| 560 |
|
| 561 |
static get_list(panel) { |
| 562 |
return panel ? panel.querySelector( this.ui.list ) : null; |
| 563 |
} |
| 564 |
|
| 565 |
static get_holder(panel) { |
| 566 |
return panel ? panel.querySelector( this.ui.holder ) : null; |
| 567 |
} |
| 568 |
|
| 569 |
static make_uid() { |
| 570 |
return 'wpbc_ins_auto_opt_' + Math.random().toString( 36 ).slice( 2, 10 ); |
| 571 |
} |
| 572 |
|
| 573 |
static append_row(panel, data) { |
| 574 |
const list = this.get_list( panel ); |
| 575 |
if ( !list ) return; |
| 576 |
|
| 577 |
const idx = list.children.length; |
| 578 |
const rowd = Object.assign( { label: '', value: '', selected: false, index: idx }, (data || {}) ); |
| 579 |
if ( !rowd.uid ) rowd.uid = this.make_uid(); |
| 580 |
|
| 581 |
const tpl_id = this.option_row_template_id; |
| 582 |
const tpl = (window.wp && wp.template) ? wp.template( tpl_id ) : null; |
| 583 |
const html = tpl ? tpl( rowd ) : null; |
| 584 |
|
| 585 |
// In append_row() -> fallback HTML. |
| 586 |
const wrap = document.createElement( 'div' ); |
| 587 |
wrap.innerHTML = html || ( |
| 588 |
'<div class="wpbc_bfb__options_row" data-index="' + (rowd.index || 0) + '">' + |
| 589 |
'<span class="wpbc_bfb__drag-handle"><span class="wpbc_icn_drag_indicator"></span></span>' + |
| 590 |
'<input type="text" class="wpbc_bfb__opt-label" placeholder="Label" value="' + (rowd.label || '') + '">' + |
| 591 |
'<input type="text" class="wpbc_bfb__opt-value" placeholder="Value" value="' + (rowd.value || '') + '">' + |
| 592 |
'<div class="wpbc_bfb__opt-selected">' + |
| 593 |
'<div class="inspector__control wpbc_ui__toggle">' + |
| 594 |
'<input type="checkbox" class="wpbc_bfb__opt-selected-chk inspector__input" id="' + rowd.uid + '" role="switch" ' + (rowd.selected ? 'checked aria-checked="true"' : 'aria-checked="false"') + '>' + |
| 595 |
'<label class="wpbc_ui__toggle_icon_radio" for="' + rowd.uid + '"></label>' + |
| 596 |
'<label class="wpbc_ui__toggle_label" for="' + rowd.uid + '">Default</label>' + |
| 597 |
'</div>' + |
| 598 |
'</div>' + |
| 599 |
// 3-dot dropdown (uses existing plugin dropdown JS). |
| 600 |
'<div class="wpbc_ui_el wpbc_ui_el_container wpbc_ui_el__dropdown">' + |
| 601 |
'<a href="javascript:void(0)" data-toggle="wpbc_dropdown" aria-expanded="false" class="ul_dropdown_menu_toggle">' + |
| 602 |
'<i class="menu_icon icon-1x wpbc_icn_more_vert"></i>' + |
| 603 |
'</a>' + |
| 604 |
'<ul class="ul_dropdown_menu" role="menu" style="right:0px; left:auto;">' + |
| 605 |
'<li>' + |
| 606 |
'<a class="ul_dropdown_menu_li_action" data-action="add_after" href="javascript:void(0)">' + |
| 607 |
'Add New' + |
| 608 |
'<i class="menu_icon icon-1x wpbc_icn_add_circle"></i>' + |
| 609 |
'</a>' + |
| 610 |
'</li>' + |
| 611 |
'<li>' + |
| 612 |
'<a class="ul_dropdown_menu_li_action" data-action="duplicate" href="javascript:void(0)">' + |
| 613 |
'Duplicate' + |
| 614 |
'<i class="menu_icon icon-1x wpbc_icn_content_copy"></i>' + |
| 615 |
'</a>' + |
| 616 |
'</li>' + |
| 617 |
'<li class="divider"></li>' + |
| 618 |
'<li>' + |
| 619 |
'<a class="ul_dropdown_menu_li_action" data-action="remove" href="javascript:void(0)">' + |
| 620 |
'Remove' + |
| 621 |
'<i class="menu_icon icon-1x wpbc_icn_delete_outline"></i>' + |
| 622 |
'</a>' + |
| 623 |
'</li>' + |
| 624 |
'</ul>' + |
| 625 |
'</div>' + |
| 626 |
'</div>' |
| 627 |
); |
| 628 |
|
| 629 |
const node = wrap.firstElementChild; |
| 630 |
if (! node) { |
| 631 |
return; |
| 632 |
} |
| 633 |
// pre-hide Value input if toggle is OFF **before** appending. |
| 634 |
const differs = this.is_value_differs_enabled( panel ); |
| 635 |
const valIn = node.querySelector( this.ui.value ); |
| 636 |
const lblIn = node.querySelector( this.ui.label ); |
| 637 |
|
| 638 |
if ( !differs && valIn ) { |
| 639 |
if ( !valIn.dataset.cached_value ) { |
| 640 |
valIn.dataset.cached_value = valIn.value || ''; |
| 641 |
} |
| 642 |
if ( lblIn ) valIn.value = this.build_value_from_label( lblIn.value, false ); |
| 643 |
valIn.setAttribute( 'disabled', 'disabled' ); |
| 644 |
valIn.style.display = 'none'; |
| 645 |
} |
| 646 |
|
| 647 |
|
| 648 |
this.init_row_fresh_flags( node ); |
| 649 |
list.appendChild( node ); |
| 650 |
|
| 651 |
// Keep your existing post-append sync as a safety net |
| 652 |
this.sync_value_inputs_visibility( panel ); |
| 653 |
} |
| 654 |
|
| 655 |
static close_dropdown(anchor_el) { |
| 656 |
try { |
| 657 |
var root = anchor_el?.closest?.( this.ui.menu_root ); |
| 658 |
if ( root ) { |
| 659 |
// If your dropdown toggler toggles a class like 'open', close it. |
| 660 |
root.classList.remove( 'open' ); |
| 661 |
// Or if it relies on aria-expanded on the toggle. |
| 662 |
var t = root.querySelector( this.ui.menu_toggle ); |
| 663 |
if ( t ) { |
| 664 |
t.setAttribute( 'aria-expanded', 'false' ); |
| 665 |
} |
| 666 |
} |
| 667 |
} catch ( _ ) { } |
| 668 |
} |
| 669 |
|
| 670 |
static insert_after(new_node, ref_node) { |
| 671 |
if ( ref_node?.parentNode ) { |
| 672 |
if ( ref_node.nextSibling ) { |
| 673 |
ref_node.parentNode.insertBefore( new_node, ref_node.nextSibling ); |
| 674 |
} else { |
| 675 |
ref_node.parentNode.appendChild( new_node ); |
| 676 |
} |
| 677 |
} |
| 678 |
} |
| 679 |
|
| 680 |
static commit_options(panel) { |
| 681 |
const list = this.get_list( panel ); |
| 682 |
const holder = this.get_holder( panel ); |
| 683 |
if ( !list || !holder ) return; |
| 684 |
|
| 685 |
const differs = this.is_value_differs_enabled( panel ); |
| 686 |
|
| 687 |
const rows = list.querySelectorAll( this.ui.row ); |
| 688 |
const options = []; |
| 689 |
for ( let i = 0; i < rows.length; i++ ) { |
| 690 |
const r = rows[i]; |
| 691 |
const lbl_in = r.querySelector( this.ui.label ); |
| 692 |
const val_in = r.querySelector( this.ui.value ); |
| 693 |
const chk = r.querySelector( this.ui.toggle ); |
| 694 |
|
| 695 |
const lbl = (lbl_in && lbl_in.value) || ''; |
| 696 |
let val = (val_in && val_in.value) || ''; |
| 697 |
|
| 698 |
// If single-input mode -> hard mirror to label. |
| 699 |
if ( ! differs ) { |
| 700 |
// single-input mode: mirror Label, minimal escaping (no slug). |
| 701 |
val = this.build_value_from_label( lbl, /*differs=*/false ); |
| 702 |
if ( val_in ) { |
| 703 |
val_in.value = val; // keep hidden input in sync for any previews/debug. |
| 704 |
} |
| 705 |
} |
| 706 |
|
| 707 |
const sel = !!(chk && chk.checked); |
| 708 |
options.push( { label: lbl, value: val, selected: sel } ); |
| 709 |
} |
| 710 |
|
| 711 |
try { |
| 712 |
holder.value = JSON.stringify( options ); |
| 713 |
holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); |
| 714 |
holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); |
| 715 |
panel.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { |
| 716 |
bubbles: true, detail: { |
| 717 |
key: 'options', value: options |
| 718 |
} |
| 719 |
} ) ); |
| 720 |
} catch ( _ ) { |
| 721 |
} |
| 722 |
|
| 723 |
this.sync_default_value_lock( panel ); |
| 724 |
this.sync_placeholder_lock( panel ); |
| 725 |
|
| 726 |
// Mirror to the selected field element so canvas/export sees current options immediately. |
| 727 |
const field = panel.__selectbase_field |
| 728 |
|| document.querySelector( '.wpbc_bfb__field.is-selected, .wpbc_bfb__field--selected' ); |
| 729 |
if ( field ) { |
| 730 |
try { |
| 731 |
field.dataset.options = JSON.stringify( options ); |
| 732 |
} catch ( _ ) { |
| 733 |
} |
| 734 |
Core.Structure?.update_field_prop?.( field, 'options', options ); |
| 735 |
field.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { |
| 736 |
bubbles: true, detail: { key: 'options', value: options } |
| 737 |
} ) ); |
| 738 |
} |
| 739 |
} |
| 740 |
|
| 741 |
|
| 742 |
static ensure_sortable(panel) { |
| 743 |
|
| 744 |
const list = this.get_list( panel ); |
| 745 |
if ( ! list ) { |
| 746 |
return; |
| 747 |
} |
| 748 |
|
| 749 |
try { |
| 750 |
const existing = window.Sortable?.get?.( list ); |
| 751 |
if ( existing ) { |
| 752 |
return; |
| 753 |
} |
| 754 |
|
| 755 |
const builder = window.wpbc_bfb_api?.get_builder?.() || window.wpbc_bfb || null; |
| 756 |
|
| 757 |
// Prefer the shared Sortable manager so the sidebar list uses |
| 758 |
// the dedicated "simple_list" config instead of the canvas config. |
| 759 |
if ( builder && builder.sortable && typeof builder.sortable.ensure === 'function' ) { |
| 760 |
|
| 761 |
builder.sortable.ensure( |
| 762 |
list, |
| 763 |
'canvas', |
| 764 |
{ |
| 765 |
sortable_kind : 'simple_list', |
| 766 |
handle_selector : this.ui.drag_handle, |
| 767 |
draggable_selector: this.ui.row, |
| 768 |
onUpdate : () => { |
| 769 |
this.commit_options( panel ); |
| 770 |
} |
| 771 |
} |
| 772 |
); |
| 773 |
|
| 774 |
} else if ( window.Sortable?.create ) { |
| 775 |
// Fallback if builder is not ready for some reason. |
| 776 |
window.Sortable.create( |
| 777 |
list, |
| 778 |
{ |
| 779 |
handle : this.ui.drag_handle, |
| 780 |
draggable : this.ui.row, |
| 781 |
animation : 120, |
| 782 |
forceFallback : true, |
| 783 |
fallbackOnBody : false, |
| 784 |
fallbackTolerance: 8, |
| 785 |
removeCloneOnHide: true, |
| 786 |
onUpdate : () => { |
| 787 |
this.commit_options( panel ); |
| 788 |
} |
| 789 |
} |
| 790 |
); |
| 791 |
} |
| 792 |
|
| 793 |
list.dataset.sortable_init = '1'; |
| 794 |
|
| 795 |
} catch ( e ) { |
| 796 |
window._wpbc?.dev?.error?.( 'Select_Base.ensure_sortable', e ); |
| 797 |
} |
| 798 |
} |
| 799 |
|
| 800 |
static rebuild_if_empty(panel) { |
| 801 |
const list = this.get_list( panel ); |
| 802 |
const holder = this.get_holder( panel ); |
| 803 |
if ( !list || !holder || list.children.length ) return; |
| 804 |
|
| 805 |
let data = []; |
| 806 |
try { |
| 807 |
data = JSON.parse( holder.value || '[]' ); |
| 808 |
} catch ( _ ) { |
| 809 |
data = []; |
| 810 |
} |
| 811 |
|
| 812 |
if ( !Array.isArray( data ) || !data.length ) { |
| 813 |
data = (this.get_defaults().options || []).slice( 0 ); |
| 814 |
try { |
| 815 |
holder.value = JSON.stringify( data ); |
| 816 |
holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); |
| 817 |
holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); |
| 818 |
} catch ( _ ) { |
| 819 |
} |
| 820 |
} |
| 821 |
|
| 822 |
for ( let i = 0; i < data.length; i++ ) { |
| 823 |
this.append_row( panel, { |
| 824 |
label : data[i]?.label || '', |
| 825 |
value : data[i]?.value || '', |
| 826 |
selected: !!data[i]?.selected, |
| 827 |
index : i, |
| 828 |
uid : this.make_uid() |
| 829 |
} ); |
| 830 |
} |
| 831 |
|
| 832 |
this.sync_default_value_lock( panel ); |
| 833 |
this.sync_placeholder_lock( panel ); |
| 834 |
this.sync_value_inputs_visibility( panel ); |
| 835 |
} |
| 836 |
|
| 837 |
static has_row_defaults(panel) { |
| 838 |
const checks = panel?.querySelectorAll( this.ui.toggle ); |
| 839 |
if ( !checks?.length ) return false; |
| 840 |
for ( let i = 0; i < checks.length; i++ ) if ( checks[i].checked ) return true; |
| 841 |
return false; |
| 842 |
} |
| 843 |
|
| 844 |
static is_multiple_enabled(panel) { |
| 845 |
const chk = panel?.querySelector( this.ui.multiple_chk ); |
| 846 |
return !!(chk && chk.checked); |
| 847 |
} |
| 848 |
|
| 849 |
static has_text_default_value(panel) { |
| 850 |
const dv = panel?.querySelector( this.ui.default_text ); |
| 851 |
return !!(dv && String( dv.value || '' ).trim().length); |
| 852 |
} |
| 853 |
|
| 854 |
static sync_default_value_lock(panel) { |
| 855 |
const input = panel?.querySelector( this.ui.default_text ); |
| 856 |
const note = panel?.querySelector( '.js-default-value-note' ); |
| 857 |
if ( !input ) return; |
| 858 |
|
| 859 |
const lock = this.has_row_defaults( panel ); |
| 860 |
input.disabled = !!lock; |
| 861 |
if ( lock ) { |
| 862 |
input.setAttribute( 'aria-disabled', 'true' ); |
| 863 |
if ( note ) note.style.display = ''; |
| 864 |
} else { |
| 865 |
input.removeAttribute( 'aria-disabled' ); |
| 866 |
if ( note ) note.style.display = 'none'; |
| 867 |
} |
| 868 |
} |
| 869 |
|
| 870 |
static sync_placeholder_lock(panel) { |
| 871 |
const input = panel?.querySelector( this.ui.placeholder_input ); |
| 872 |
const note = panel?.querySelector( this.ui.placeholder_note ); |
| 873 |
|
| 874 |
// NEW: compute multiple and toggle row visibility |
| 875 |
const isMultiple = this.is_multiple_enabled( panel ); |
| 876 |
const placeholderRow = input?.closest( '.inspector__row' ) || null; |
| 877 |
const sizeInput = panel?.querySelector( this.ui.size_input ) || null; |
| 878 |
const sizeRow = sizeInput?.closest( '.inspector__row' ) || null; |
| 879 |
|
| 880 |
// Show placeholder only for single-select; show size only for multiple |
| 881 |
if ( placeholderRow ) placeholderRow.style.display = isMultiple ? 'none' : ''; |
| 882 |
if ( sizeRow ) sizeRow.style.display = isMultiple ? '' : 'none'; |
| 883 |
|
| 884 |
// Existing behavior (keep as-is) |
| 885 |
if ( !input ) return; |
| 886 |
|
| 887 |
const lock = isMultiple || this.has_row_defaults( panel ) || this.has_text_default_value( panel ); |
| 888 |
if ( note && !note.id ) note.id = 'wpbc_placeholder_note_' + Math.random().toString( 36 ).slice( 2, 10 ); |
| 889 |
|
| 890 |
input.disabled = !!lock; |
| 891 |
if ( lock ) { |
| 892 |
input.setAttribute( 'aria-disabled', 'true' ); |
| 893 |
if ( note ) { |
| 894 |
note.style.display = ''; |
| 895 |
input.setAttribute( 'aria-describedby', note.id ); |
| 896 |
} |
| 897 |
} else { |
| 898 |
input.removeAttribute( 'aria-disabled' ); |
| 899 |
input.removeAttribute( 'aria-describedby' ); |
| 900 |
if ( note ) note.style.display = 'none'; |
| 901 |
} |
| 902 |
} |
| 903 |
|
| 904 |
static enforce_single_default(panel, clicked) { |
| 905 |
if ( this.is_multiple_enabled( panel ) ) return; |
| 906 |
|
| 907 |
const checks = panel?.querySelectorAll( this.ui.toggle ); |
| 908 |
if ( !checks?.length ) return; |
| 909 |
|
| 910 |
if ( clicked && clicked.checked ) { |
| 911 |
for ( let i = 0; i < checks.length; i++ ) if ( checks[i] !== clicked ) { |
| 912 |
checks[i].checked = false; |
| 913 |
checks[i].setAttribute( 'aria-checked', 'false' ); |
| 914 |
} |
| 915 |
clicked.setAttribute( 'aria-checked', 'true' ); |
| 916 |
return; |
| 917 |
} |
| 918 |
|
| 919 |
let kept = false; |
| 920 |
for ( let j = 0; j < checks.length; j++ ) if ( checks[j].checked ) { |
| 921 |
if ( !kept ) { |
| 922 |
kept = true; |
| 923 |
} else { |
| 924 |
checks[j].checked = false; |
| 925 |
checks[j].setAttribute( 'aria-checked', 'false' ); |
| 926 |
} |
| 927 |
} |
| 928 |
|
| 929 |
this.sync_default_value_lock( panel ); |
| 930 |
this.sync_placeholder_lock( panel ); |
| 931 |
} |
| 932 |
|
| 933 |
// ---- one-time bootstrap of a panel ---- |
| 934 |
static bootstrap_panel(panel) { |
| 935 |
if ( !panel ) return; |
| 936 |
if ( !panel.querySelector( '.wpbc_bfb__options_editor' ) ) return; // only select-like UIs |
| 937 |
if ( panel.dataset.selectbase_bootstrapped === '1' ) { |
| 938 |
this.ensure_sortable( panel ); |
| 939 |
return; |
| 940 |
} |
| 941 |
|
| 942 |
this.rebuild_if_empty( panel ); |
| 943 |
this.ensure_sortable( panel ); |
| 944 |
panel.dataset.selectbase_bootstrapped = '1'; |
| 945 |
|
| 946 |
this.sync_default_value_lock( panel ); |
| 947 |
this.sync_placeholder_lock( panel ); |
| 948 |
this.sync_value_inputs_visibility( panel ); |
| 949 |
} |
| 950 |
|
| 951 |
// ---- hook into inspector lifecycle (fires ONCE) ---- |
| 952 |
static wire_once() { |
| 953 |
if ( Core.__selectbase_wired ) return; |
| 954 |
Core.__selectbase_wired = true; |
| 955 |
|
| 956 |
const on_ready_or_render = (ev) => { |
| 957 |
const panel = ev?.detail?.panel; |
| 958 |
const field = ev?.detail?.el || ev?.detail?.field || null; |
| 959 |
if ( !panel ) return; |
| 960 |
if ( field ) panel.__selectbase_field = field; |
| 961 |
this.bootstrap_panel( panel ); |
| 962 |
// If the inspector root was remounted, ensure root listeners are (re)bound. |
| 963 |
this.wire_root_listeners(); |
| 964 |
}; |
| 965 |
|
| 966 |
document.addEventListener( 'wpbc_bfb_inspector_ready', on_ready_or_render ); |
| 967 |
document.addEventListener( 'wpbc_bfb_inspector_render', on_ready_or_render ); |
| 968 |
|
| 969 |
this.wire_root_listeners(); |
| 970 |
} |
| 971 |
|
| 972 |
static wire_root_listeners() { |
| 973 |
|
| 974 |
// If already wired AND the stored root is still in the DOM, bail out. |
| 975 |
if ( this.__root_wired && this.__root_node?.isConnected ) return; |
| 976 |
|
| 977 |
const root = document.getElementById( 'wpbc_bfb__inspector' ); |
| 978 |
if ( !root ) { |
| 979 |
// Root missing (e.g., SPA re-render) — clear flags so we can wire later. |
| 980 |
this.__root_wired = false; |
| 981 |
this.__root_node = null; |
| 982 |
return; |
| 983 |
} |
| 984 |
|
| 985 |
this.__root_node = root; |
| 986 |
this.__root_wired = true; |
| 987 |
root.dataset.selectbase_root_wired = '1'; |
| 988 |
|
| 989 |
const get_panel = (target) => |
| 990 |
target?.closest?.( '.wpbc_bfb__inspector__body' ) || |
| 991 |
root.querySelector( '.wpbc_bfb__inspector__body' ) || null; |
| 992 |
|
| 993 |
// Click handlers: add / delete / duplicate |
| 994 |
root.addEventListener( 'click', (e) => { |
| 995 |
const panel = get_panel( e.target ); |
| 996 |
if ( !panel ) return; |
| 997 |
|
| 998 |
this.bootstrap_panel( panel ); |
| 999 |
|
| 1000 |
const ui = this.ui; |
| 1001 |
|
| 1002 |
// Existing "Add option" button (top toolbar) |
| 1003 |
const add = e.target.closest?.( ui.add_btn ); |
| 1004 |
if ( add ) { |
| 1005 |
this.append_row( panel, { label: '', value: '', selected: false } ); |
| 1006 |
this.commit_options( panel ); |
| 1007 |
this.sync_value_inputs_visibility( panel ); |
| 1008 |
return; |
| 1009 |
} |
| 1010 |
|
| 1011 |
// Dropdown menu actions. |
| 1012 |
const menu_action = e.target.closest?.( ui.menu_action ); |
| 1013 |
if ( menu_action ) { |
| 1014 |
e.preventDefault(); |
| 1015 |
e.stopPropagation(); |
| 1016 |
|
| 1017 |
const action = (menu_action.getAttribute( 'data-action' ) || '').toLowerCase(); |
| 1018 |
const row = menu_action.closest?.( ui.row ); |
| 1019 |
|
| 1020 |
if ( !row ) { |
| 1021 |
this.close_dropdown( menu_action ); |
| 1022 |
return; |
| 1023 |
} |
| 1024 |
|
| 1025 |
if ( 'add_after' === action ) { |
| 1026 |
// Add empty row after current |
| 1027 |
const prev_count = this.get_list( panel )?.children.length || 0; |
| 1028 |
this.append_row( panel, { label: '', value: '', selected: false } ); |
| 1029 |
// Move the newly added last row just after current row to preserve "add after" |
| 1030 |
const list = this.get_list( panel ); |
| 1031 |
if ( list && list.lastElementChild && list.lastElementChild !== row ) { |
| 1032 |
this.insert_after( list.lastElementChild, row ); |
| 1033 |
} |
| 1034 |
this.commit_options( panel ); |
| 1035 |
this.sync_value_inputs_visibility( panel ); |
| 1036 |
} else if ( 'duplicate' === action ) { |
| 1037 |
const lbl = (row.querySelector( ui.label ) || {}).value || ''; |
| 1038 |
const val = (row.querySelector( ui.value ) || {}).value || ''; |
| 1039 |
const sel = !!((row.querySelector( ui.toggle ) || {}).checked); |
| 1040 |
this.append_row( panel, { label: lbl, value: val, selected: sel, uid: this.make_uid() } ); |
| 1041 |
// Place the new row right after the current. |
| 1042 |
const list = this.get_list( panel ); |
| 1043 |
|
| 1044 |
if ( list && list.lastElementChild && list.lastElementChild !== row ) { |
| 1045 |
this.insert_after( list.lastElementChild, row ); |
| 1046 |
} |
| 1047 |
this.enforce_single_default( panel, null ); |
| 1048 |
this.commit_options( panel ); |
| 1049 |
this.sync_value_inputs_visibility( panel ); |
| 1050 |
} else if ( 'remove' === action ) { |
| 1051 |
if ( row && row.parentNode ) row.parentNode.removeChild( row ); |
| 1052 |
this.commit_options( panel ); |
| 1053 |
this.sync_value_inputs_visibility( panel ); |
| 1054 |
} |
| 1055 |
|
| 1056 |
this.close_dropdown( menu_action ); |
| 1057 |
return; |
| 1058 |
} |
| 1059 |
|
| 1060 |
}, true ); |
| 1061 |
|
| 1062 |
|
| 1063 |
// Input delegation. |
| 1064 |
root.addEventListener( 'input', (e) => { |
| 1065 |
const panel = get_panel( e.target ); |
| 1066 |
if ( ! panel ) { |
| 1067 |
return; |
| 1068 |
} |
| 1069 |
const ui = this.ui; |
| 1070 |
const is_label_or_value = e.target.classList?.contains( 'wpbc_bfb__opt-label' ) || e.target.classList?.contains( 'wpbc_bfb__opt-value' ); |
| 1071 |
const is_toggle = e.target.classList?.contains( 'wpbc_bfb__opt-selected-chk' ); |
| 1072 |
const is_multiple = e.target.matches?.( ui.multiple_chk ); |
| 1073 |
const is_default_text = e.target.matches?.( ui.default_text ); |
| 1074 |
const is_value_differs = e.target.matches?.( ui.value_differs_chk ); |
| 1075 |
|
| 1076 |
// Handle "value differs" toggle live |
| 1077 |
if ( is_value_differs ) { |
| 1078 |
this.sync_value_inputs_visibility( panel ); |
| 1079 |
this.commit_options( panel ); |
| 1080 |
return; |
| 1081 |
} |
| 1082 |
|
| 1083 |
// Track when the user edits VALUE explicitly |
| 1084 |
if ( e.target.classList?.contains( 'wpbc_bfb__opt-value' ) ) { |
| 1085 |
const row = e.target.closest( this.ui.row ); |
| 1086 |
this.mark_row_value_user_touched( row ); |
| 1087 |
// Keep the cache updated so toggling OFF/ON later restores the latest custom value |
| 1088 |
e.target.dataset.cached_value = e.target.value || ''; |
| 1089 |
} |
| 1090 |
|
| 1091 |
// Auto-fill VALUE from LABEL if value is fresh (and differs is ON); if differs is OFF, we mirror anyway in commit |
| 1092 |
if ( e.target.classList?.contains( 'wpbc_bfb__opt-label' ) ) { |
| 1093 |
const row = e.target.closest( ui.row ); |
| 1094 |
const val_in = row?.querySelector( ui.value ); |
| 1095 |
const differs = this.is_value_differs_enabled( panel ); |
| 1096 |
|
| 1097 |
if ( val_in ) { |
| 1098 |
if ( !differs ) { |
| 1099 |
// single-input mode: mirror human label with minimal escaping |
| 1100 |
val_in.value = this.build_value_from_label( e.target.value, false ); |
| 1101 |
} else if ( !this.is_row_value_user_touched( row ) ) { |
| 1102 |
// separate-value mode, only while fresh |
| 1103 |
val_in.value = this.build_value_from_label( e.target.value, true ); |
| 1104 |
} |
| 1105 |
} |
| 1106 |
} |
| 1107 |
|
| 1108 |
|
| 1109 |
if ( is_label_or_value || is_toggle || is_multiple ) { |
| 1110 |
if ( is_toggle ) e.target.setAttribute( 'aria-checked', e.target.checked ? 'true' : 'false' ); |
| 1111 |
if ( is_toggle || is_multiple ) this.enforce_single_default( panel, is_toggle ? e.target : null ); |
| 1112 |
this.commit_options( panel ); |
| 1113 |
} |
| 1114 |
|
| 1115 |
if ( is_default_text ) { |
| 1116 |
this.sync_default_value_lock( panel ); |
| 1117 |
this.sync_placeholder_lock( panel ); |
| 1118 |
const holder = this.get_holder( panel ); |
| 1119 |
if ( holder ) { |
| 1120 |
holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); |
| 1121 |
holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); |
| 1122 |
} |
| 1123 |
} |
| 1124 |
}, true ); |
| 1125 |
|
| 1126 |
|
| 1127 |
// Change delegation |
| 1128 |
root.addEventListener( 'change', (e) => { |
| 1129 |
const panel = get_panel( e.target ); |
| 1130 |
if ( !panel ) return; |
| 1131 |
|
| 1132 |
const ui = this.ui; |
| 1133 |
const is_toggle = e.target.classList?.contains( 'wpbc_bfb__opt-selected-chk' ); |
| 1134 |
const is_multi = e.target.matches?.( ui.multiple_chk ); |
| 1135 |
if ( !is_toggle && !is_multi ) return; |
| 1136 |
|
| 1137 |
if ( is_toggle ) e.target.setAttribute( 'aria-checked', e.target.checked ? 'true' : 'false' ); |
| 1138 |
this.enforce_single_default( panel, is_toggle ? e.target : null ); |
| 1139 |
this.commit_options( panel ); |
| 1140 |
}, true ); |
| 1141 |
|
| 1142 |
// Lazy bootstrap |
| 1143 |
root.addEventListener( 'mouseenter', (e) => { |
| 1144 |
const panel = get_panel( e.target ); |
| 1145 |
if ( panel && e.target?.closest?.( this.ui.list ) ) this.bootstrap_panel( panel ); |
| 1146 |
}, true ); |
| 1147 |
|
| 1148 |
root.addEventListener( 'mousedown', (e) => { |
| 1149 |
const panel = get_panel( e.target ); |
| 1150 |
if ( panel && e.target?.closest?.( this.ui.drag_handle ) ) this.bootstrap_panel( panel ); |
| 1151 |
}, true ); |
| 1152 |
} |
| 1153 |
|
| 1154 |
}; |
| 1155 |
|
| 1156 |
try { Core.WPBC_BFB_Select_Base.wire_once(); } catch (_) {} |
| 1157 |
// Try immediately (if root is already in DOM), then again on DOMContentLoaded. |
| 1158 |
Core.WPBC_BFB_Select_Base.wire_root_listeners(); |
| 1159 |
|
| 1160 |
document.addEventListener('DOMContentLoaded', () => { Core.WPBC_BFB_Select_Base.wire_root_listeners(); }); |
| 1161 |
|
| 1162 |
}( window )); |