| 1 |
/* globals window, document */ |
| 2 |
(function (w, d) { |
| 3 |
'use strict'; |
| 4 |
|
| 5 |
/** |
| 6 |
* BFB Form Settings UI bridge. |
| 7 |
* |
| 8 |
* Listens to: |
| 9 |
* - wpbc:bfb:form_settings:apply (from AJAX load) -> apply to controls |
| 10 |
* - wpbc:bfb:form_settings:collect (from AJAX save) -> collect from controls |
| 11 |
* |
| 12 |
* Optional: |
| 13 |
* - re-apply after Builder STRUCTURE_LOADED (timing hook only) |
| 14 |
*/ |
| 15 |
const api = (w.WPBC_BFB_FormSettings = w.WPBC_BFB_FormSettings || {}); |
| 16 |
|
| 17 |
// Last received settings pack (from AJAX). |
| 18 |
let last_settings_pack = null; |
| 19 |
|
| 20 |
// Small retry, because DOM can be re-rendered after apply event. |
| 21 |
let raf_id = 0; |
| 22 |
let retry_count = 0; |
| 23 |
const retry_max = 20; |
| 24 |
|
| 25 |
// ----------------------------------------------------------------------------------------------- |
| 26 |
// Small helpers |
| 27 |
// ----------------------------------------------------------------------------------------------- |
| 28 |
|
| 29 |
function query_all(root, selector) { |
| 30 |
return Array.from((root || d).querySelectorAll(selector)); |
| 31 |
} |
| 32 |
|
| 33 |
function css_escape(value) { |
| 34 |
const v = String(value == null ? '' : value); |
| 35 |
if (w.CSS && typeof w.CSS.escape === 'function') return w.CSS.escape(v); |
| 36 |
return v.replace(/[^a-zA-Z0-9_\-]/g, '\\$&'); |
| 37 |
} |
| 38 |
|
| 39 |
function is_on(value) { |
| 40 |
const v = String(value == null ? '' : value).trim().toLowerCase(); |
| 41 |
return (v === 'on' || v === '1' || v === 'true' || v === 'yes'); |
| 42 |
} |
| 43 |
|
| 44 |
function set_initial_attr(el, value) { |
| 45 |
if (!el) return; |
| 46 |
el.setAttribute('data-wpbc-bfb-fs-initial', String(value == null ? '' : value)); |
| 47 |
} |
| 48 |
|
| 49 |
function trigger_change(el) { |
| 50 |
if (!el) return; |
| 51 |
try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) {} |
| 52 |
} |
| 53 |
|
| 54 |
function trigger_input(el) { |
| 55 |
if ( !el ) return; |
| 56 |
try { el.dispatchEvent( new Event( 'input', { bubbles: true } ) ); } catch ( _ ) {} |
| 57 |
} |
| 58 |
|
| 59 |
function find_rows(scope) { |
| 60 |
const rows = query_all(d, '.wpbc-setting[data-key]'); |
| 61 |
if (!scope) return rows; |
| 62 |
|
| 63 |
return rows.filter(function (row) { |
| 64 |
return String(row.getAttribute('data-scope') || '') === String(scope); |
| 65 |
}); |
| 66 |
} |
| 67 |
|
| 68 |
function has_any_rows() { |
| 69 |
return query_all(d, '.wpbc-setting[data-key]').length > 0; |
| 70 |
} |
| 71 |
|
| 72 |
// ----------------------------------------------------------------------------------------------- |
| 73 |
// Row setter |
| 74 |
// ----------------------------------------------------------------------------------------------- |
| 75 |
|
| 76 |
function set_value_for_row(row, value, opts) { |
| 77 |
if (!row) return; |
| 78 |
|
| 79 |
const row_type = String(row.getAttribute('data-type') || ''); |
| 80 |
const row_key = String(row.getAttribute('data-key') || ''); |
| 81 |
const do_trigger_events = !!(opts && opts.trigger_change); |
| 82 |
|
| 83 |
if (!row_key) return; |
| 84 |
|
| 85 |
// Radio group |
| 86 |
if (row_type === 'radio') { |
| 87 |
const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]'); |
| 88 |
const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : ''; |
| 89 |
if (!control_id) return; |
| 90 |
|
| 91 |
const target_value = String(value == null ? '' : value); |
| 92 |
const radios = query_all(row, 'input[type="radio"][name="' + css_escape(control_id) + '"]'); |
| 93 |
|
| 94 |
let checked_radio = null; |
| 95 |
radios.forEach(function (radio) { |
| 96 |
const should_check = (String(radio.value) === target_value); |
| 97 |
radio.checked = should_check; |
| 98 |
if (should_check) checked_radio = radio; |
| 99 |
}); |
| 100 |
|
| 101 |
if (wrap) set_initial_attr(wrap, target_value); |
| 102 |
if (do_trigger_events && checked_radio) trigger_change(checked_radio); |
| 103 |
return; |
| 104 |
} |
| 105 |
|
| 106 |
// Toggle |
| 107 |
if (row_type === 'toggle') { |
| 108 |
const checkbox = |
| 109 |
row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') || |
| 110 |
row.querySelector('input[type="checkbox"]'); |
| 111 |
|
| 112 |
if (!checkbox) return; |
| 113 |
|
| 114 |
const checked = is_on(value); |
| 115 |
checkbox.checked = checked; |
| 116 |
checkbox.setAttribute('aria-checked', checked ? 'true' : 'false'); |
| 117 |
|
| 118 |
set_initial_attr(checkbox, checked ? 'On' : 'Off'); |
| 119 |
if (do_trigger_events) trigger_change(checkbox); |
| 120 |
return; |
| 121 |
} |
| 122 |
|
| 123 |
// Select |
| 124 |
if (row_type === 'select') { |
| 125 |
const select = |
| 126 |
row.querySelector('select[data-wpbc-bfb-fs-type="select"]') || |
| 127 |
row.querySelector('select'); |
| 128 |
|
| 129 |
if (!select) return; |
| 130 |
|
| 131 |
select.value = String(value == null ? '' : value); |
| 132 |
set_initial_attr(select, select.value); |
| 133 |
if (do_trigger_events) trigger_change(select); |
| 134 |
return; |
| 135 |
} |
| 136 |
|
| 137 |
// Length: hidden combined + num/unit |
| 138 |
if ( row_type === 'length' ) { |
| 139 |
// JS slider length control: - hidden writer carries FS markers and must receive input event so wpbc_slider_len_groups.js syncs UI. |
| 140 |
const writer = |
| 141 |
row.querySelector( 'input[data-wpbc_slider_len_writer][data-wpbc-bfb-fs-type="length"]' ) || |
| 142 |
row.querySelector( 'input[data-wpbc-bfb-fs-type="length"]' ); |
| 143 |
if ( !writer ) return; |
| 144 |
|
| 145 |
const combined = String( value == null ? '' : value ); |
| 146 |
writer.value = combined; |
| 147 |
set_initial_attr( writer, combined ); |
| 148 |
if ( do_trigger_events ) trigger_input( writer ); |
| 149 |
return; |
| 150 |
} |
| 151 |
|
| 152 |
// Range (slider number): writer is the number input. |
| 153 |
if (row_type === 'range') { |
| 154 |
const writer = |
| 155 |
row.querySelector('input[data-wpbc_slider_range_writer]') || |
| 156 |
row.querySelector('input[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') || |
| 157 |
row.querySelector('input[type="number"]'); |
| 158 |
if (!writer) return; |
| 159 |
|
| 160 |
writer.value = String(value == null ? '' : value); |
| 161 |
set_initial_attr(writer, writer.value); |
| 162 |
if (do_trigger_events) trigger_input(writer); |
| 163 |
return; |
| 164 |
} |
| 165 |
|
| 166 |
// Default: input/textarea |
| 167 |
const control = |
| 168 |
row.querySelector('[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') || |
| 169 |
row.querySelector('input,textarea'); |
| 170 |
|
| 171 |
if (!control) return; |
| 172 |
|
| 173 |
control.value = String(value == null ? '' : value); |
| 174 |
set_initial_attr(control, control.value); |
| 175 |
// For normal inputs, "input" gives better reactivity than "change". |
| 176 |
if (do_trigger_events) trigger_input(control); |
| 177 |
} |
| 178 |
|
| 179 |
// ----------------------------------------------------------------------------------------------- |
| 180 |
// Public API |
| 181 |
// ----------------------------------------------------------------------------------------------- |
| 182 |
|
| 183 |
/** |
| 184 |
* Apply FLAT object (key=>value) to rows of some scope. |
| 185 |
*/ |
| 186 |
function apply_flat_settings(flat_settings, scope, opts) { |
| 187 |
if (!flat_settings || typeof flat_settings !== 'object') return; |
| 188 |
|
| 189 |
find_rows(scope).forEach(function (row) { |
| 190 |
const key = String(row.getAttribute('data-key') || ''); |
| 191 |
if (!key) return; |
| 192 |
if (!Object.prototype.hasOwnProperty.call(flat_settings, key)) return; |
| 193 |
set_value_for_row(row, flat_settings[key], opts); |
| 194 |
}); |
| 195 |
} |
| 196 |
|
| 197 |
/** |
| 198 |
* Apply settings. |
| 199 |
* |
| 200 |
* Supports: |
| 201 |
* - flat: { booking_form_layout_width: '100%', ... } |
| 202 |
* - pack: { options: {...}, css_vars: {...} } (Option A) |
| 203 |
*/ |
| 204 |
api.apply = function (settings_pack, scope, opts) { |
| 205 |
if (!settings_pack || typeof settings_pack !== 'object') return; |
| 206 |
if (!settings_pack.options || typeof settings_pack.options !== 'object') return; // strict Option A |
| 207 |
apply_flat_settings(settings_pack.options, scope || 'form', opts); |
| 208 |
}; |
| 209 |
|
| 210 |
/** |
| 211 |
* Collect current values (flat object). |
| 212 |
*/ |
| 213 |
api.collect = function (scope) { |
| 214 |
const out = {}; |
| 215 |
|
| 216 |
find_rows(scope || 'form').forEach(function (row) { |
| 217 |
const key = String(row.getAttribute('data-key') || ''); |
| 218 |
const type = String(row.getAttribute('data-type') || ''); |
| 219 |
if (!key) return; |
| 220 |
|
| 221 |
if (type === 'radio') { |
| 222 |
const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]'); |
| 223 |
const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : ''; |
| 224 |
if (!control_id) return; |
| 225 |
|
| 226 |
const checked = row.querySelector('input[type="radio"][name="' + css_escape(control_id) + '"]:checked'); |
| 227 |
out[key] = checked ? String(checked.value) : ''; |
| 228 |
return; |
| 229 |
} |
| 230 |
|
| 231 |
if (type === 'toggle') { |
| 232 |
const checkbox = |
| 233 |
row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') || |
| 234 |
row.querySelector('input[type="checkbox"]'); |
| 235 |
out[key] = checkbox && checkbox.checked ? 'On' : 'Off'; |
| 236 |
return; |
| 237 |
} |
| 238 |
|
| 239 |
if (type === 'select') { |
| 240 |
const select = row.querySelector('select'); |
| 241 |
out[key] = select ? String(select.value) : ''; |
| 242 |
return; |
| 243 |
} |
| 244 |
|
| 245 |
if (type === 'length') { |
| 246 |
const hidden = row.querySelector('input[data-wpbc-bfb-fs-type="length"]'); |
| 247 |
out[key] = hidden ? String(hidden.value || '') : ''; |
| 248 |
return; |
| 249 |
} |
| 250 |
|
| 251 |
if (type === 'range') { |
| 252 |
const writer = |
| 253 |
row.querySelector('input[data-wpbc_slider_range_writer]') || |
| 254 |
row.querySelector('input[type="number"]') || |
| 255 |
row.querySelector('input[type="range"]'); |
| 256 |
out[key] = writer ? String(writer.value || '') : ''; |
| 257 |
return; |
| 258 |
} |
| 259 |
const control = row.querySelector('input,textarea'); |
| 260 |
out[key] = control ? String(control.value || '') : ''; |
| 261 |
}); |
| 262 |
|
| 263 |
return out; |
| 264 |
}; |
| 265 |
|
| 266 |
/** |
| 267 |
* Re-apply last received settings (useful after DOM re-render). |
| 268 |
*/ |
| 269 |
api.reapply_last = function () { |
| 270 |
if (!last_settings_pack) return; |
| 271 |
api.apply(last_settings_pack, 'form', { trigger_change: true }); |
| 272 |
}; |
| 273 |
|
| 274 |
api.init = function () { |
| 275 |
// If apply event fired before init, try again now. |
| 276 |
if (last_settings_pack) schedule_apply_retry(); |
| 277 |
}; |
| 278 |
|
| 279 |
// ----------------------------------------------------------------------------------------------- |
| 280 |
// DOM Events (AJAX layer) |
| 281 |
// ----------------------------------------------------------------------------------------------- |
| 282 |
|
| 283 |
// Save: let modules contribute into { options:{}, css_vars:{} } |
| 284 |
d.addEventListener('wpbc:bfb:form_settings:collect', function (e) { |
| 285 |
const detail = (e && e.detail) ? e.detail : {}; |
| 286 |
const target_pack = detail.settings; |
| 287 |
|
| 288 |
if (!target_pack || typeof target_pack !== 'object') return; |
| 289 |
|
| 290 |
// Option A: write into target_pack.options |
| 291 |
if (!target_pack.options || typeof target_pack.options !== 'object') { |
| 292 |
target_pack.options = {}; |
| 293 |
} |
| 294 |
|
| 295 |
const collected = api.collect('form'); |
| 296 |
Object.keys(collected).forEach(function (k) { |
| 297 |
target_pack.options[k] = collected[k]; |
| 298 |
}); |
| 299 |
}); |
| 300 |
|
| 301 |
// Load: receive settings from AJAX and apply. |
| 302 |
d.addEventListener('wpbc:bfb:form_settings:apply', function (e) { |
| 303 |
const detail = (e && e.detail) ? e.detail : {}; |
| 304 |
|
| 305 |
last_settings_pack = detail.settings || null; |
| 306 |
|
| 307 |
retry_count = 0; |
| 308 |
schedule_apply_retry(); |
| 309 |
}); |
| 310 |
|
| 311 |
function schedule_apply_retry() { |
| 312 |
if (raf_id) return; |
| 313 |
|
| 314 |
raf_id = w.requestAnimationFrame(function () { |
| 315 |
raf_id = 0; |
| 316 |
|
| 317 |
// If settings UI not present yet, retry a few frames. |
| 318 |
if (!has_any_rows()) { |
| 319 |
retry_count++; |
| 320 |
if (retry_count < retry_max) schedule_apply_retry(); |
| 321 |
return; |
| 322 |
} |
| 323 |
|
| 324 |
api.reapply_last(); |
| 325 |
}); |
| 326 |
} |
| 327 |
|
| 328 |
// ----------------------------------------------------------------------------------------------- |
| 329 |
// Optional Builder timing hook (STRUCTURE_LOADED) -> reapply_last() |
| 330 |
// ----------------------------------------------------------------------------------------------- |
| 331 |
|
| 332 |
function bind_builder_timing_hook(builder_instance) { |
| 333 |
const core = w.WPBC_BFB_Core; |
| 334 |
const events = (core && core.WPBC_BFB_Events) ? core.WPBC_BFB_Events : (w.WPBC_BFB_Events || null); |
| 335 |
|
| 336 |
|
| 337 |
if (!builder_instance || !builder_instance.bus || !events || !events.STRUCTURE_LOADED) return; |
| 338 |
|
| 339 |
builder_instance.bus.on(events.STRUCTURE_LOADED, function () { |
| 340 |
// Builder may re-render settings panel after structure load. |
| 341 |
// Re-apply last settings pack (if any). |
| 342 |
retry_count = 0; |
| 343 |
schedule_apply_retry(); |
| 344 |
}); |
| 345 |
} |
| 346 |
|
| 347 |
if (w.wpbc_bfb_api && w.wpbc_bfb_api.ready && typeof w.wpbc_bfb_api.ready.then === 'function') { |
| 348 |
w.wpbc_bfb_api.ready.then(bind_builder_timing_hook); |
| 349 |
} else { |
| 350 |
setTimeout(function () { if (w.__B) { bind_builder_timing_hook( w.__B ); } }, 0); |
| 351 |
} |
| 352 |
|
| 353 |
// DOM ready init. |
| 354 |
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', api.init); |
| 355 |
else api.init(); |
| 356 |
|
| 357 |
})(window, document); |
| 358 |
|