// --------------------------------------------------------------------------------------------------------------------- // == File /includes/page-form-builder/_out/core/bfb-fields.js == | 2025-09-10 15:47 // --------------------------------------------------------------------------------------------------------------------- (function ( w ) { 'use strict'; // Single global namespace (idempotent & load-order safe). const Core = ( w.WPBC_BFB_Core = w.WPBC_BFB_Core || {} ); const UI = ( Core.UI = Core.UI || {} ); /** * Base class for field renderers (static-only contract). * ================================================================================================================ * Contract exposed to the builder (static methods on the CLASS itself): * - render(el, data, ctx) // REQUIRED * - on_field_drop(data, el, meta) // OPTIONAL (default provided) * * Helpers for subclasses: * - get_defaults() -> per-field defaults (MUST override in subclass to set type/label) * - normalize_data(d) -> shallow merge with defaults * - get_template(id) -> per-id cached wp.template compiler * * Subclass usage: * class WPBC_BFB_Field_Text extends Core.WPBC_BFB_Field_Base { static get_defaults(){ ... } } * WPBC_BFB_Field_Text.template_id = 'wpbc-bfb-field-text'; * ================================================================================================================ */ Core.WPBC_BFB_Field_Base = class { /** * Default field data (generic baseline). * Subclasses MUST override to provide { type, label } appropriate for the field. * @returns {Object} */ static get_defaults() { return { type : 'field', label : 'Field', name : 'field', html_id : '', placeholder : '', required : false, minlength : '', maxlength : '', pattern : '', cssclass : '', help : '' }; } /** * Shallow-merge incoming data with defaults. * @param {Object} data * @returns {Object} */ static normalize_data( data ) { var d = data || {}; var defaults = this.get_defaults(); var out = {}; var k; for ( k in defaults ) { if ( Object.prototype.hasOwnProperty.call( defaults, k ) ) { out[k] = defaults[k]; } } for ( k in d ) { if ( Object.prototype.hasOwnProperty.call( d, k ) ) { out[k] = d[k]; } } return out; } /** * Compile and cache a wp.template by id (per-id cache). * @param {string} template_id * @returns {Function|null} */ static get_template(template_id) { // Accept either "wpbc-bfb-field-text" or "tmpl-wpbc-bfb-field-text". if ( ! template_id || ! window.wp || ! wp.template ) { return null; } const domId = template_id.startsWith( 'tmpl-' ) ? template_id : ('tmpl-' + template_id); if ( ! document.getElementById( domId ) ) { return null; } if ( ! Core.__bfb_tpl_cache_map ) { Core.__bfb_tpl_cache_map = {}; } // Normalize id for the compiler & cache. // wp.template expects id WITHOUT the "tmpl-" prefix ! const key = template_id.replace( /^tmpl-/, '' ); if ( Core.__bfb_tpl_cache_map[key] ) { return Core.__bfb_tpl_cache_map[key]; } const compiler = wp.template( key ); // <-- normalized id here if ( compiler ) { Core.__bfb_tpl_cache_map[key] = compiler; } return compiler; } /** * REQUIRED: render preview into host element (full redraw; idempotent). * Subclasses should set static `template_id` to a valid wp.template id. * @param {HTMLElement} el * @param {Object} data * @param {{mode?:string,builder?:any,tpl?:Function,sanit?:any}} ctx * @returns {void} */ static render( el, data, ctx ) { if ( ! el ) { return; } var compile = this.get_template( this.template_id ); var d = this.normalize_data( data ); var s = (ctx && ctx.sanit) ? ctx.sanit : Core.WPBC_BFB_Sanitize; // Sanitize critical attributes before templating. if ( s ) { d.html_id = d.html_id ? s.sanitize_html_id( String( d.html_id ) ) : ''; d.name = s.sanitize_html_name( String( d.name || d.id || 'field' ) ); } else { d.html_id = d.html_id ? String( d.html_id ) : ''; d.name = String( d.name || d.id || 'field' ); } // Fall back to generic preview if template not available. if ( compile ) { el.innerHTML = compile( d ); // After render, set attribute values via DOM so quotes/newlines are handled correctly. const input = el.querySelector( 'input, textarea, select' ); if ( input ) { if ( d.placeholder != null ) input.setAttribute( 'placeholder', String( d.placeholder ) ); if ( d.title != null ) input.setAttribute( 'title', String( d.title ) ); } } else { el.innerHTML = Core.WPBC_Form_Builder_Helper.render_field_inner_html( d ); } el.dataset.type = d.type || 'field'; el.setAttribute( 'data-label', (d.label != null ? String( d.label ) : '') ); // allow "". } /** * OPTIONAL hook executed after field is dropped/loaded/preview. * Default extended: * - On first drop: stamp default label (existing behavior) and mark field as "fresh" for auto-name. * - On load: mark as loaded so later label edits do not rename the saved name. */ static on_field_drop(data, el, meta) { const context = (meta && meta.context) ? String( meta.context ) : ''; // ----------------------------------------------------------------------------------------- // NEW: Seed default "help" (and keep it in Structure) for all field packs that define it. // This fixes the mismatch where: // - UI shows default help via normalize_data() / templates // - but get_structure() / exporters see `help` as undefined/empty. // // Behavior: // - Runs ONLY on initial drop (context === 'drop'). // - If get_defaults() exposes a non-empty "help", and data.help is // missing / null / empty string -> we persist the default into `data` // and notify Structure so exports see it. // - On "load" we do nothing, so existing forms where user *cleared* // help will not be overridden. // ----------------------------------------------------------------------------------------- if ( context === 'drop' && data ) { try { const defs = (typeof this.get_defaults === 'function') ? this.get_defaults() : null; if ( defs && Object.prototype.hasOwnProperty.call( defs, 'help' ) ) { const current = Object.prototype.hasOwnProperty.call( data, 'help' ) ? data.help : undefined; const hasValue = (current !== undefined && current !== null && String( current ) !== ''); const defaultVal = defs.help; if ( ! hasValue && defaultVal != null && String( defaultVal ) !== '' ) { // 1) persist into data object (used by Structure). data.help = defaultVal; // 2) mirror into dataset (for any DOM-based consumers). if ( el ) { el.dataset.help = String( defaultVal ); // 3) notify Structure / listeners (if available). try { Core.Structure?.update_field_prop?.( el, 'help', defaultVal ); el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail : { key: 'help', value: defaultVal } } ) ); } catch ( _inner ) {} } } } } catch ( _e ) {} } // ----------------------------------------------------------------------------------------- if ( context === 'drop' && !Object.prototype.hasOwnProperty.call( data, 'label' ) ) { const defs = this.get_defaults(); data.label = defs.label || 'Field'; el.setAttribute( 'data-label', data.label ); } // Mark provenance flags. if ( context === 'drop' ) { el.dataset.fresh = '1'; // can auto-name on first label edit. el.dataset.autoname = '1'; el.dataset.was_loaded = '0'; // Seed a provisional unique name immediately. try { const b = meta?.builder; if ( b?.id && (!el.hasAttribute( 'data-name' ) || !el.getAttribute( 'data-name' )) ) { const S = Core.WPBC_BFB_Sanitize; const base = S.sanitize_html_name( el.getAttribute( 'data-id' ) || data?.id || data?.type || 'field' ); const uniq = b.id.ensure_unique_field_name( base, el ); el.setAttribute( 'data-name', uniq ); el.dataset.name_user_touched = '0'; } } catch ( _ ) {} } else if ( context === 'load' ) { el.dataset.fresh = '0'; el.dataset.autoname = '0'; el.dataset.was_loaded = '1'; // never rename names for loaded fields. } } // --- Auto Rename "Fresh" field, on entering the new Label --- /** * Create a conservative field "name" from a human label. * Uses the same constraints as sanitize_html_name (letters/digits/_- and leading letter). */ static name_from_label(label) { const s = Core.WPBC_BFB_Sanitize.sanitize_html_name( String( label ?? '' ) ); return s.toLowerCase() || 'field'; } /** * Auto-fill data-name from label ONLY for freshly dropped fields that were not edited yet. * - Never runs for sections. * - Never runs for loaded/existing fields. * - Stops as soon as user edits the Name manually. * * @param {WPBC_Form_Builder} builder * @param {HTMLElement} el - .wpbc_bfb__field element * @param {string} labelVal */ static maybe_autoname_from_label(builder, el, labelVal) { if ( !builder || !el ) return; if ( el.classList.contains( 'wpbc_bfb__section' ) ) return; const allowAuto = el.dataset.autoname === '1'; const userTouched = el.dataset.name_user_touched === '1'; const isLoaded = el.dataset.was_loaded === '1'; if ( !allowAuto || userTouched || isLoaded ) return; // Only override placeholder-y names const S = Core.WPBC_BFB_Sanitize; const base = this.name_from_label( labelVal ); const unique = builder.id.ensure_unique_field_name( base, el ); el.setAttribute( 'data-name', unique ); const ins = document.getElementById( 'wpbc_bfb__inspector' ); const nameCtrl = ins?.querySelector( '[data-inspector-key="name"]' ); if ( nameCtrl && 'value' in nameCtrl && nameCtrl.value !== unique ) nameCtrl.value = unique; } }; /** * Select_Base (shared base for select-like packs) * * @type {Core.WPBC_BFB_Select_Base} */ Core.WPBC_BFB_Select_Base = class extends Core.WPBC_BFB_Field_Base { static template_id = null; // main preview template id static option_row_template_id = 'wpbc-bfb-inspector-select-option-row'; // row tpl id static kind = 'select'; static __root_wired = false; static __root_node = null; // Single source of selectors used by the inspector UI. static ui = { list : '.wpbc_bfb__options_list', holder : '.wpbc_bfb__options_state[data-inspector-key="options"]', row : '.wpbc_bfb__options_row', label : '.wpbc_bfb__opt-label', value : '.wpbc_bfb__opt-value', toggle : '.wpbc_bfb__opt-selected-chk', add_btn: '.js-add-option', drag_handle : '.wpbc_bfb__drag-handle', multiple_chk : '.js-opt-multiple[data-inspector-key="multiple"]', default_text : '.js-default-value[data-inspector-key="default_value"]', placeholder_input: '.js-placeholder[data-inspector-key="placeholder"]', placeholder_note : '.js-placeholder-note', size_input : '.inspector__input[data-inspector-key="size"]', // Dropdown menu integration. menu_root : '.wpbc_ui_el__dropdown', menu_toggle: '[data-toggle="wpbc_dropdown"]', menu_action: '.ul_dropdown_menu_li_action[data-action]', // Value-differs toggle. value_differs_chk: '.js-value-differs[data-inspector-key="value_differs"]', }; /** * Build option value from label. * - If `differs === true` -> generate token (slug-like machine value). * - If `differs === false` -> keep human text; escape only dangerous chars. * @param {string} label * @param {boolean} differs * @returns {string} */ static build_value_from_label(label, differs) { const S = Core.WPBC_BFB_Sanitize; if ( differs ) { return (S && typeof S.to_token === 'function') ? S.to_token( String( label || '' ) ) : String( label || '' ).trim().toLowerCase().replace( /\s+/g, '_' ).replace( /[^\w-]/g, '' ); } // single-input mode: keep human text; template will escape safely. return String( label == null ? '' : label ); } /** * Is the “value differs from label” toggle enabled? * @param {HTMLElement} panel * @returns {boolean} */ static is_value_differs_enabled(panel) { const chk = panel?.querySelector( this.ui.value_differs_chk ); return !!(chk && chk.checked); } /** * Ensure visibility/enabled state of Value inputs based on the toggle. * When disabled -> hide Value inputs and keep them mirrored from Label. * @param {HTMLElement} panel * @returns {void} */ static sync_value_inputs_visibility(panel) { const differs = this.is_value_differs_enabled( panel ); const rows = panel?.querySelectorAll( this.ui.row ) || []; for ( let i = 0; i < rows.length; i++ ) { const r = rows[i]; const lbl_in = r.querySelector( this.ui.label ); const val_in = r.querySelector( this.ui.value ); if ( !val_in ) continue; if ( differs ) { // Re-enable & show value input val_in.removeAttribute( 'disabled' ); val_in.style.display = ''; // If we have a cached custom value and the row wasn't edited while OFF, restore it const hasCache = !!val_in.dataset.cached_value; const userEdited = r.dataset.value_user_touched === '1'; if ( hasCache && !userEdited ) { val_in.value = val_in.dataset.cached_value; } else if ( !hasCache ) { // No cache: if value is just a mirrored label, offer a tokenized default const lbl = lbl_in ? lbl_in.value : ''; const mirrored = this.build_value_from_label( lbl, /*differs=*/false ); if ( val_in.value === mirrored ) { val_in.value = this.build_value_from_label( lbl, /*differs=*/true ); } } } else { // ON -> OFF: cache once, then mirror if ( !val_in.dataset.cached_value ) { val_in.dataset.cached_value = val_in.value || ''; } const lbl = lbl_in ? lbl_in.value : ''; val_in.value = this.build_value_from_label( lbl, /*differs=*/false ); val_in.setAttribute( 'disabled', 'disabled' ); val_in.style.display = 'none'; // NOTE: do NOT mark as user_touched here } } } /** * Return whether this row’s value has been edited by user. * @param {HTMLElement} row * @returns {boolean} */ static is_row_value_user_touched(row) { return row?.dataset?.value_user_touched === '1'; } /** * Mark this row’s value as edited by user. * @param {HTMLElement} row */ static mark_row_value_user_touched(row) { if ( row ) row.dataset.value_user_touched = '1'; } /** * Initialize “freshness” flags on a row (value untouched). * Call on creation/append of rows. * @param {HTMLElement} row */ static init_row_fresh_flags(row) { if ( row ) { if ( !row.dataset.value_user_touched ) { row.dataset.value_user_touched = '0'; } } } // ---- defaults (packs can override) ---- static get_defaults() { return { type : this.kind, label : 'Select', name : '', html_id : '', placeholder : '--- Select ---', required : false, multiple : false, size : null, cssclass : '', help : '', default_value: '', options : [ { label: 'Option 1', value: 'Option 1', selected: false }, { label: 'Option 2', value: 'Option 2', selected: false }, { label: 'Option 3', value: 'Option 3', selected: false }, { label: 'Option 4', value: 'Option 4', selected: false } ], min_width : '240px' }; } // ---- preview render (idempotent) ---- static render(el, data, ctx) { if ( !el ) return; const d = this.normalize_data( data ); if ( d.min_width != null ) { el.dataset.min_width = String( d.min_width ); try { el.style.setProperty( '--wpbc-col-min', String( d.min_width ) ); } catch ( _ ) { } } if ( d.html_id != null ) el.dataset.html_id = String( d.html_id || '' ); if ( d.cssclass != null ) el.dataset.cssclass = String( d.cssclass || '' ); if ( d.placeholder != null ) el.dataset.placeholder = String( d.placeholder || '' ); const tpl = this.get_template( this.template_id ); if ( typeof tpl !== 'function' ) { el.innerHTML = ''; return; } try { el.innerHTML = tpl( d ); } catch ( e ) { window._wpbc?.dev?.error?.( 'Select_Base.render', e ); el.innerHTML = ''; return; } el.dataset.type = d.type || this.kind; el.setAttribute( 'data-label', (d.label != null ? String( d.label ) : '') ); try { Core.UI?.WPBC_BFB_Overlay?.ensure?.( ctx?.builder, el ); } catch ( _ ) { } if ( !el.dataset.options && Array.isArray( d.options ) && d.options.length ) { try { el.dataset.options = JSON.stringify( d.options ); } catch ( _ ) { } } } // ---- drop seeding (options + placeholder) ---- static on_field_drop(data, el, meta) { try { super.on_field_drop?.( data, el, meta ); } catch ( _ ) { } const is_drop = (meta && meta.context === 'drop'); if ( is_drop ) { if ( !Array.isArray( data.options ) || !data.options.length ) { const opts = (this.get_defaults().options || []).map( (o) => ({ label : o.label, value : o.value, selected: !!o.selected }) ); data.options = opts; try { el.dataset.options = JSON.stringify( opts ); el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail : { key : 'options', value: opts } } ) ); Core.Structure?.update_field_prop?.( el, 'options', opts ); } catch ( _ ) { } } const ph = (data.placeholder ?? '').toString().trim(); if ( !ph ) { const dflt = this.get_defaults().placeholder || '--- Select ---'; data.placeholder = dflt; try { el.dataset.placeholder = String( dflt ); el.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail : { key : 'placeholder', value: dflt } } ) ); Core.Structure?.update_field_prop?.( el, 'placeholder', dflt ); } catch ( _ ) { } } } } // ============================== // Inspector helpers (snake_case) // ============================== static get_panel_root(el) { return el?.closest?.( '.wpbc_bfb__inspector__body' ) || el?.closest?.( '.wpbc_bfb__inspector' ) || null; } static get_list(panel) { return panel ? panel.querySelector( this.ui.list ) : null; } static get_holder(panel) { return panel ? panel.querySelector( this.ui.holder ) : null; } static make_uid() { return 'wpbc_ins_auto_opt_' + Math.random().toString( 36 ).slice( 2, 10 ); } static append_row(panel, data) { const list = this.get_list( panel ); if ( !list ) return; const idx = list.children.length; const rowd = Object.assign( { label: '', value: '', selected: false, index: idx }, (data || {}) ); if ( !rowd.uid ) rowd.uid = this.make_uid(); const tpl_id = this.option_row_template_id; const tpl = (window.wp && wp.template) ? wp.template( tpl_id ) : null; const html = tpl ? tpl( rowd ) : null; // In append_row() -> fallback HTML. const wrap = document.createElement( 'div' ); wrap.innerHTML = html || ( '
' + '' + '' + '' + '
' + '
' + '' + '' + '' + '
' + '
' + // 3-dot dropdown (uses existing plugin dropdown JS). '
' + '' + '' + '
' + '
' ); const node = wrap.firstElementChild; if (! node) { return; } // pre-hide Value input if toggle is OFF **before** appending. const differs = this.is_value_differs_enabled( panel ); const valIn = node.querySelector( this.ui.value ); const lblIn = node.querySelector( this.ui.label ); if ( !differs && valIn ) { if ( !valIn.dataset.cached_value ) { valIn.dataset.cached_value = valIn.value || ''; } if ( lblIn ) valIn.value = this.build_value_from_label( lblIn.value, false ); valIn.setAttribute( 'disabled', 'disabled' ); valIn.style.display = 'none'; } this.init_row_fresh_flags( node ); list.appendChild( node ); // Keep your existing post-append sync as a safety net this.sync_value_inputs_visibility( panel ); } static close_dropdown(anchor_el) { try { var root = anchor_el?.closest?.( this.ui.menu_root ); if ( root ) { // If your dropdown toggler toggles a class like 'open', close it. root.classList.remove( 'open' ); // Or if it relies on aria-expanded on the toggle. var t = root.querySelector( this.ui.menu_toggle ); if ( t ) { t.setAttribute( 'aria-expanded', 'false' ); } } } catch ( _ ) { } } static insert_after(new_node, ref_node) { if ( ref_node?.parentNode ) { if ( ref_node.nextSibling ) { ref_node.parentNode.insertBefore( new_node, ref_node.nextSibling ); } else { ref_node.parentNode.appendChild( new_node ); } } } static commit_options(panel) { const list = this.get_list( panel ); const holder = this.get_holder( panel ); if ( !list || !holder ) return; const differs = this.is_value_differs_enabled( panel ); const rows = list.querySelectorAll( this.ui.row ); const options = []; for ( let i = 0; i < rows.length; i++ ) { const r = rows[i]; const lbl_in = r.querySelector( this.ui.label ); const val_in = r.querySelector( this.ui.value ); const chk = r.querySelector( this.ui.toggle ); const lbl = (lbl_in && lbl_in.value) || ''; let val = (val_in && val_in.value) || ''; // If single-input mode -> hard mirror to label. if ( ! differs ) { // single-input mode: mirror Label, minimal escaping (no slug). val = this.build_value_from_label( lbl, /*differs=*/false ); if ( val_in ) { val_in.value = val; // keep hidden input in sync for any previews/debug. } } const sel = !!(chk && chk.checked); options.push( { label: lbl, value: val, selected: sel } ); } try { holder.value = JSON.stringify( options ); holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); panel.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail: { key: 'options', value: options } } ) ); } catch ( _ ) { } this.sync_default_value_lock( panel ); this.sync_placeholder_lock( panel ); // Mirror to the selected field element so canvas/export sees current options immediately. const field = panel.__selectbase_field || document.querySelector( '.wpbc_bfb__field.is-selected, .wpbc_bfb__field--selected' ); if ( field ) { try { field.dataset.options = JSON.stringify( options ); } catch ( _ ) { } Core.Structure?.update_field_prop?.( field, 'options', options ); field.dispatchEvent( new CustomEvent( 'wpbc_bfb_field_data_changed', { bubbles: true, detail: { key: 'options', value: options } } ) ); } } static ensure_sortable(panel) { const list = this.get_list( panel ); if ( ! list ) { return; } try { const existing = window.Sortable?.get?.( list ); if ( existing ) { return; } const builder = window.wpbc_bfb_api?.get_builder?.() || window.wpbc_bfb || null; // Prefer the shared Sortable manager so the sidebar list uses // the dedicated "simple_list" config instead of the canvas config. if ( builder && builder.sortable && typeof builder.sortable.ensure === 'function' ) { builder.sortable.ensure( list, 'canvas', { sortable_kind : 'simple_list', handle_selector : this.ui.drag_handle, draggable_selector: this.ui.row, onUpdate : () => { this.commit_options( panel ); } } ); } else if ( window.Sortable?.create ) { // Fallback if builder is not ready for some reason. window.Sortable.create( list, { handle : this.ui.drag_handle, draggable : this.ui.row, animation : 120, forceFallback : true, fallbackOnBody : false, fallbackTolerance: 8, removeCloneOnHide: true, onUpdate : () => { this.commit_options( panel ); } } ); } list.dataset.sortable_init = '1'; } catch ( e ) { window._wpbc?.dev?.error?.( 'Select_Base.ensure_sortable', e ); } } static rebuild_if_empty(panel) { const list = this.get_list( panel ); const holder = this.get_holder( panel ); if ( !list || !holder || list.children.length ) return; let data = []; try { data = JSON.parse( holder.value || '[]' ); } catch ( _ ) { data = []; } if ( !Array.isArray( data ) || !data.length ) { data = (this.get_defaults().options || []).slice( 0 ); try { holder.value = JSON.stringify( data ); holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } catch ( _ ) { } } for ( let i = 0; i < data.length; i++ ) { this.append_row( panel, { label : data[i]?.label || '', value : data[i]?.value || '', selected: !!data[i]?.selected, index : i, uid : this.make_uid() } ); } this.sync_default_value_lock( panel ); this.sync_placeholder_lock( panel ); this.sync_value_inputs_visibility( panel ); } static has_row_defaults(panel) { const checks = panel?.querySelectorAll( this.ui.toggle ); if ( !checks?.length ) return false; for ( let i = 0; i < checks.length; i++ ) if ( checks[i].checked ) return true; return false; } static is_multiple_enabled(panel) { const chk = panel?.querySelector( this.ui.multiple_chk ); return !!(chk && chk.checked); } static has_text_default_value(panel) { const dv = panel?.querySelector( this.ui.default_text ); return !!(dv && String( dv.value || '' ).trim().length); } static sync_default_value_lock(panel) { const input = panel?.querySelector( this.ui.default_text ); const note = panel?.querySelector( '.js-default-value-note' ); if ( !input ) return; const lock = this.has_row_defaults( panel ); input.disabled = !!lock; if ( lock ) { input.setAttribute( 'aria-disabled', 'true' ); if ( note ) note.style.display = ''; } else { input.removeAttribute( 'aria-disabled' ); if ( note ) note.style.display = 'none'; } } static sync_placeholder_lock(panel) { const input = panel?.querySelector( this.ui.placeholder_input ); const note = panel?.querySelector( this.ui.placeholder_note ); // NEW: compute multiple and toggle row visibility const isMultiple = this.is_multiple_enabled( panel ); const placeholderRow = input?.closest( '.inspector__row' ) || null; const sizeInput = panel?.querySelector( this.ui.size_input ) || null; const sizeRow = sizeInput?.closest( '.inspector__row' ) || null; // Show placeholder only for single-select; show size only for multiple if ( placeholderRow ) placeholderRow.style.display = isMultiple ? 'none' : ''; if ( sizeRow ) sizeRow.style.display = isMultiple ? '' : 'none'; // Existing behavior (keep as-is) if ( !input ) return; const lock = isMultiple || this.has_row_defaults( panel ) || this.has_text_default_value( panel ); if ( note && !note.id ) note.id = 'wpbc_placeholder_note_' + Math.random().toString( 36 ).slice( 2, 10 ); input.disabled = !!lock; if ( lock ) { input.setAttribute( 'aria-disabled', 'true' ); if ( note ) { note.style.display = ''; input.setAttribute( 'aria-describedby', note.id ); } } else { input.removeAttribute( 'aria-disabled' ); input.removeAttribute( 'aria-describedby' ); if ( note ) note.style.display = 'none'; } } static enforce_single_default(panel, clicked) { if ( this.is_multiple_enabled( panel ) ) return; const checks = panel?.querySelectorAll( this.ui.toggle ); if ( !checks?.length ) return; if ( clicked && clicked.checked ) { for ( let i = 0; i < checks.length; i++ ) if ( checks[i] !== clicked ) { checks[i].checked = false; checks[i].setAttribute( 'aria-checked', 'false' ); } clicked.setAttribute( 'aria-checked', 'true' ); return; } let kept = false; for ( let j = 0; j < checks.length; j++ ) if ( checks[j].checked ) { if ( !kept ) { kept = true; } else { checks[j].checked = false; checks[j].setAttribute( 'aria-checked', 'false' ); } } this.sync_default_value_lock( panel ); this.sync_placeholder_lock( panel ); } // ---- one-time bootstrap of a panel ---- static bootstrap_panel(panel) { if ( !panel ) return; if ( !panel.querySelector( '.wpbc_bfb__options_editor' ) ) return; // only select-like UIs if ( panel.dataset.selectbase_bootstrapped === '1' ) { this.ensure_sortable( panel ); return; } this.rebuild_if_empty( panel ); this.ensure_sortable( panel ); panel.dataset.selectbase_bootstrapped = '1'; this.sync_default_value_lock( panel ); this.sync_placeholder_lock( panel ); this.sync_value_inputs_visibility( panel ); } // ---- hook into inspector lifecycle (fires ONCE) ---- static wire_once() { if ( Core.__selectbase_wired ) return; Core.__selectbase_wired = true; const on_ready_or_render = (ev) => { const panel = ev?.detail?.panel; const field = ev?.detail?.el || ev?.detail?.field || null; if ( !panel ) return; if ( field ) panel.__selectbase_field = field; this.bootstrap_panel( panel ); // If the inspector root was remounted, ensure root listeners are (re)bound. this.wire_root_listeners(); }; document.addEventListener( 'wpbc_bfb_inspector_ready', on_ready_or_render ); document.addEventListener( 'wpbc_bfb_inspector_render', on_ready_or_render ); this.wire_root_listeners(); } static wire_root_listeners() { // If already wired AND the stored root is still in the DOM, bail out. if ( this.__root_wired && this.__root_node?.isConnected ) return; const root = document.getElementById( 'wpbc_bfb__inspector' ); if ( !root ) { // Root missing (e.g., SPA re-render) — clear flags so we can wire later. this.__root_wired = false; this.__root_node = null; return; } this.__root_node = root; this.__root_wired = true; root.dataset.selectbase_root_wired = '1'; const get_panel = (target) => target?.closest?.( '.wpbc_bfb__inspector__body' ) || root.querySelector( '.wpbc_bfb__inspector__body' ) || null; // Click handlers: add / delete / duplicate root.addEventListener( 'click', (e) => { const panel = get_panel( e.target ); if ( !panel ) return; this.bootstrap_panel( panel ); const ui = this.ui; // Existing "Add option" button (top toolbar) const add = e.target.closest?.( ui.add_btn ); if ( add ) { this.append_row( panel, { label: '', value: '', selected: false } ); this.commit_options( panel ); this.sync_value_inputs_visibility( panel ); return; } // Dropdown menu actions. const menu_action = e.target.closest?.( ui.menu_action ); if ( menu_action ) { e.preventDefault(); e.stopPropagation(); const action = (menu_action.getAttribute( 'data-action' ) || '').toLowerCase(); const row = menu_action.closest?.( ui.row ); if ( !row ) { this.close_dropdown( menu_action ); return; } if ( 'add_after' === action ) { // Add empty row after current const prev_count = this.get_list( panel )?.children.length || 0; this.append_row( panel, { label: '', value: '', selected: false } ); // Move the newly added last row just after current row to preserve "add after" const list = this.get_list( panel ); if ( list && list.lastElementChild && list.lastElementChild !== row ) { this.insert_after( list.lastElementChild, row ); } this.commit_options( panel ); this.sync_value_inputs_visibility( panel ); } else if ( 'duplicate' === action ) { const lbl = (row.querySelector( ui.label ) || {}).value || ''; const val = (row.querySelector( ui.value ) || {}).value || ''; const sel = !!((row.querySelector( ui.toggle ) || {}).checked); this.append_row( panel, { label: lbl, value: val, selected: sel, uid: this.make_uid() } ); // Place the new row right after the current. const list = this.get_list( panel ); if ( list && list.lastElementChild && list.lastElementChild !== row ) { this.insert_after( list.lastElementChild, row ); } this.enforce_single_default( panel, null ); this.commit_options( panel ); this.sync_value_inputs_visibility( panel ); } else if ( 'remove' === action ) { if ( row && row.parentNode ) row.parentNode.removeChild( row ); this.commit_options( panel ); this.sync_value_inputs_visibility( panel ); } this.close_dropdown( menu_action ); return; } }, true ); // Input delegation. root.addEventListener( 'input', (e) => { const panel = get_panel( e.target ); if ( ! panel ) { return; } const ui = this.ui; const is_label_or_value = e.target.classList?.contains( 'wpbc_bfb__opt-label' ) || e.target.classList?.contains( 'wpbc_bfb__opt-value' ); const is_toggle = e.target.classList?.contains( 'wpbc_bfb__opt-selected-chk' ); const is_multiple = e.target.matches?.( ui.multiple_chk ); const is_default_text = e.target.matches?.( ui.default_text ); const is_value_differs = e.target.matches?.( ui.value_differs_chk ); // Handle "value differs" toggle live if ( is_value_differs ) { this.sync_value_inputs_visibility( panel ); this.commit_options( panel ); return; } // Track when the user edits VALUE explicitly if ( e.target.classList?.contains( 'wpbc_bfb__opt-value' ) ) { const row = e.target.closest( this.ui.row ); this.mark_row_value_user_touched( row ); // Keep the cache updated so toggling OFF/ON later restores the latest custom value e.target.dataset.cached_value = e.target.value || ''; } // Auto-fill VALUE from LABEL if value is fresh (and differs is ON); if differs is OFF, we mirror anyway in commit if ( e.target.classList?.contains( 'wpbc_bfb__opt-label' ) ) { const row = e.target.closest( ui.row ); const val_in = row?.querySelector( ui.value ); const differs = this.is_value_differs_enabled( panel ); if ( val_in ) { if ( !differs ) { // single-input mode: mirror human label with minimal escaping val_in.value = this.build_value_from_label( e.target.value, false ); } else if ( !this.is_row_value_user_touched( row ) ) { // separate-value mode, only while fresh val_in.value = this.build_value_from_label( e.target.value, true ); } } } if ( is_label_or_value || is_toggle || is_multiple ) { if ( is_toggle ) e.target.setAttribute( 'aria-checked', e.target.checked ? 'true' : 'false' ); if ( is_toggle || is_multiple ) this.enforce_single_default( panel, is_toggle ? e.target : null ); this.commit_options( panel ); } if ( is_default_text ) { this.sync_default_value_lock( panel ); this.sync_placeholder_lock( panel ); const holder = this.get_holder( panel ); if ( holder ) { holder.dispatchEvent( new Event( 'input', { bubbles: true } ) ); holder.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } } }, true ); // Change delegation root.addEventListener( 'change', (e) => { const panel = get_panel( e.target ); if ( !panel ) return; const ui = this.ui; const is_toggle = e.target.classList?.contains( 'wpbc_bfb__opt-selected-chk' ); const is_multi = e.target.matches?.( ui.multiple_chk ); if ( !is_toggle && !is_multi ) return; if ( is_toggle ) e.target.setAttribute( 'aria-checked', e.target.checked ? 'true' : 'false' ); this.enforce_single_default( panel, is_toggle ? e.target : null ); this.commit_options( panel ); }, true ); // Lazy bootstrap root.addEventListener( 'mouseenter', (e) => { const panel = get_panel( e.target ); if ( panel && e.target?.closest?.( this.ui.list ) ) this.bootstrap_panel( panel ); }, true ); root.addEventListener( 'mousedown', (e) => { const panel = get_panel( e.target ); if ( panel && e.target?.closest?.( this.ui.drag_handle ) ) this.bootstrap_panel( panel ); }, true ); } }; try { Core.WPBC_BFB_Select_Base.wire_once(); } catch (_) {} // Try immediately (if root is already in DOM), then again on DOMContentLoaded. Core.WPBC_BFB_Select_Base.wire_root_listeners(); document.addEventListener('DOMContentLoaded', () => { Core.WPBC_BFB_Select_Base.wire_root_listeners(); }); }( window ));