| 1 |
/* |
| 2 |
* Advanced Booking form mode. |
| 3 |
* |
| 4 |
* Refactored: module-style (DOM / State / Editor / Sync / Clipboard / Events / UI) |
| 5 |
* |
| 6 |
* @file ../includes/page-form-builder/advanced-mode/_out/bfb-advanced-form-editor.js |
| 7 |
*/ |
| 8 |
(function (w, d) { |
| 9 |
'use strict'; |
| 10 |
|
| 11 |
// True after wpbc:bfb:structure:loaded fired at least once. |
| 12 |
var structure_loaded_once = false; |
| 13 |
|
| 14 |
function adapted_has_any_field(adapted) { |
| 15 |
if ( ! adapted || ! Array.isArray( adapted.pages ) ) { |
| 16 |
return false; |
| 17 |
} |
| 18 |
|
| 19 |
var has = false; |
| 20 |
|
| 21 |
function walk_section(sec) { |
| 22 |
if ( has || ! sec ) { return; } |
| 23 |
var cols = Array.isArray( sec.columns ) ? sec.columns : []; |
| 24 |
for ( var i = 0; i < cols.length; i++ ) { |
| 25 |
var col = cols[i] || {}; |
| 26 |
if ( Array.isArray( col.fields ) && col.fields.length ) { has = true; return; } |
| 27 |
var nested = Array.isArray( col.sections ) ? col.sections : []; |
| 28 |
for ( var j = 0; j < nested.length; j++ ) { |
| 29 |
walk_section( nested[j] ); |
| 30 |
if ( has ) { return; } |
| 31 |
} |
| 32 |
} |
| 33 |
} |
| 34 |
|
| 35 |
for ( var p = 0; p < adapted.pages.length; p++ ) { |
| 36 |
var page = adapted.pages[p] || {}; |
| 37 |
var items = Array.isArray( page.items ) ? page.items : []; |
| 38 |
for ( var k = 0; k < items.length; k++ ) { |
| 39 |
var it = items[k] || {}; |
| 40 |
if ( it.kind === 'field' ) { return true; } |
| 41 |
if ( it.kind === 'section' ) { |
| 42 |
walk_section( it.data ); |
| 43 |
if ( has ) { return true; } |
| 44 |
} |
| 45 |
} |
| 46 |
} |
| 47 |
return has; |
| 48 |
} |
| 49 |
|
| 50 |
// == Constants ================================================================================================== |
| 51 |
var IDS = { |
| 52 |
panels : 'wpbc_bfb__advanced_mode_panels', |
| 53 |
ta_form : 'wpbc_bfb__advanced_form_editor', |
| 54 |
ta_content : 'wpbc_bfb__content_form_editor', |
| 55 |
btn_regen : 'wpbc_bfb__advanced_regenerate_btn', |
| 56 |
btn_copy_form: 'wpbc_bfb__advanced_copy_form_btn', |
| 57 |
btn_copy_cnt : 'wpbc_bfb__advanced_copy_content_btn', |
| 58 |
cb_autosync : 'wpbc_bfb__advanced_autosync', |
| 59 |
dirty_hint : 'wpbc_bfb__advanced_dirty_hint' |
| 60 |
}; |
| 61 |
|
| 62 |
var KEY = { |
| 63 |
FORM : 'advanced_form', |
| 64 |
CONTENT: 'content_form' |
| 65 |
}; |
| 66 |
|
| 67 |
var TA_ID_BY_KEY = {}; |
| 68 |
TA_ID_BY_KEY[KEY.FORM] = IDS.ta_form; |
| 69 |
TA_ID_BY_KEY[KEY.CONTENT] = IDS.ta_content; |
| 70 |
|
| 71 |
// == DOM helpers ================================================================================================ |
| 72 |
var DOM = (function () { |
| 73 |
|
| 74 |
function get_by_id(id) { |
| 75 |
return d.getElementById( id ); |
| 76 |
} |
| 77 |
|
| 78 |
function on(el, type, fn) { |
| 79 |
if ( el ) { |
| 80 |
el.addEventListener( type, fn ); |
| 81 |
} |
| 82 |
} |
| 83 |
|
| 84 |
function is_advanced_ui_present() { |
| 85 |
return !! d.querySelector( '#' + IDS.panels ); |
| 86 |
} |
| 87 |
|
| 88 |
return { |
| 89 |
get : get_by_id, |
| 90 |
on : on, |
| 91 |
has_ui: is_advanced_ui_present |
| 92 |
}; |
| 93 |
})(); |
| 94 |
|
| 95 |
// == State ====================================================================================================== |
| 96 |
var State = (function () { |
| 97 |
|
| 98 |
var state = { |
| 99 |
editors : {}, // key -> wp.codeEditor instance (or null) |
| 100 |
autosync_user_value : null, // null = not decided by user, boolean = user explicitly set |
| 101 |
sync_state_bootstrapped: false, |
| 102 |
is_dirty : false, |
| 103 |
is_programmatic_update : false, |
| 104 |
is_inited : false, |
| 105 |
textarea_fallback_bound: false |
| 106 |
}; |
| 107 |
|
| 108 |
state.editors[KEY.FORM] = null; |
| 109 |
state.editors[KEY.CONTENT] = null; |
| 110 |
|
| 111 |
function set_live_badges() { |
| 112 |
var cb = DOM.get( IDS.cb_autosync ); |
| 113 |
var sync_on = !! (cb && cb.checked); |
| 114 |
|
| 115 |
var live = (sync_on && ! state.is_dirty) ? 'builder' : 'advanced'; |
| 116 |
|
| 117 |
var root = d.documentElement; |
| 118 |
if ( ! root ) { |
| 119 |
return; |
| 120 |
} |
| 121 |
|
| 122 |
root.setAttribute( 'data-wpbc-bfb-live-source', live === 'builder' ? 'builder' : 'advanced' ); |
| 123 |
root.setAttribute( 'data-wpbc-bfb-sync-mode', sync_on ? 'on' : 'off' ); |
| 124 |
} |
| 125 |
|
| 126 |
function set_dirty(on) { |
| 127 |
state.is_dirty = !! on; |
| 128 |
|
| 129 |
var hint = DOM.get( IDS.dirty_hint ); |
| 130 |
if ( hint ) { |
| 131 |
hint.style.display = state.is_dirty ? 'inline' : 'none'; |
| 132 |
} |
| 133 |
|
| 134 |
var autosync = DOM.get( IDS.cb_autosync ); |
| 135 |
|
| 136 |
if ( state.is_dirty ) { |
| 137 |
// User edited Advanced => explicit opt-out of autosync for this session. |
| 138 |
state.autosync_user_value = false; |
| 139 |
state.sync_state_bootstrapped = true; |
| 140 |
|
| 141 |
if ( autosync ) { |
| 142 |
autosync.checked = false; |
| 143 |
} |
| 144 |
} else { |
| 145 |
// IMPORTANT: |
| 146 |
// When clearing dirty state, do NOT force autosync checkbox ON. |
| 147 |
// Checkbox state is controlled by: |
| 148 |
// - user action (cb change) |
| 149 |
// - Sync.apply_autosync_state() when autosync_user_value === null |
| 150 |
} |
| 151 |
|
| 152 |
set_live_badges(); |
| 153 |
} |
| 154 |
|
| 155 |
|
| 156 |
function is_autosync_on() { |
| 157 |
var cb = DOM.get( IDS.cb_autosync ); |
| 158 |
return !! (cb && cb.checked && ! state.is_dirty); |
| 159 |
} |
| 160 |
|
| 161 |
return { |
| 162 |
raw : state, |
| 163 |
set_dirty : set_dirty, |
| 164 |
is_autosync_on: is_autosync_on, |
| 165 |
update_badges : set_live_badges |
| 166 |
}; |
| 167 |
})(); |
| 168 |
|
| 169 |
// == Editor (CodeMirror + textarea fallback + shortcode highlighting mode) ====================================== |
| 170 |
var Editor = (function () { |
| 171 |
|
| 172 |
var is_oshortcode_defined = false; |
| 173 |
|
| 174 |
function can_init_codemirror() { |
| 175 |
var wpns = w.wp || null; |
| 176 |
return !! ( |
| 177 |
wpns && |
| 178 |
wpns.codeEditor && |
| 179 |
typeof wpns.codeEditor.initialize === 'function' && |
| 180 |
w.wpbc_bfb_code_editor_settings |
| 181 |
); |
| 182 |
} |
| 183 |
|
| 184 |
function ensure_oshortcode_mode() { |
| 185 |
if ( is_oshortcode_defined ) { |
| 186 |
return; |
| 187 |
} |
| 188 |
|
| 189 |
var CM = w.wp && w.wp.CodeMirror ? w.wp.CodeMirror : null; |
| 190 |
if ( ! CM || typeof CM.defineMode !== 'function' ) { |
| 191 |
return; |
| 192 |
} |
| 193 |
|
| 194 |
CM.defineMode( 'oshortcode', function (config, parserConfig) { |
| 195 |
|
| 196 |
var overlay = { |
| 197 |
token: function (stream) { |
| 198 |
var ch; |
| 199 |
|
| 200 |
// [name ...] or [name* ...] |
| 201 |
if ( stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/ ) ) { |
| 202 |
while ( (ch = stream.next()) != null ) { |
| 203 |
if ( ch === ']' ) { |
| 204 |
return 'oshortcode'; |
| 205 |
} |
| 206 |
} |
| 207 |
} |
| 208 |
|
| 209 |
while ( stream.next() != null && ! stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/, false ) ) {} |
| 210 |
return null; |
| 211 |
} |
| 212 |
}; |
| 213 |
|
| 214 |
var base = CM.getMode( config, (parserConfig && parserConfig.backdrop) || 'htmlmixed' ); |
| 215 |
|
| 216 |
// Fallback if overlay addon is missing. |
| 217 |
if ( typeof CM.overlayMode !== 'function' ) { |
| 218 |
return base; |
| 219 |
} |
| 220 |
|
| 221 |
return CM.overlayMode( base, overlay ); |
| 222 |
} ); |
| 223 |
|
| 224 |
is_oshortcode_defined = true; |
| 225 |
} |
| 226 |
|
| 227 |
function bind_textarea_dirty_fallback() { |
| 228 |
if ( State.raw.textarea_fallback_bound ) { |
| 229 |
return; |
| 230 |
} |
| 231 |
|
| 232 |
var ta_form = DOM.get( IDS.ta_form ); |
| 233 |
var ta_cnt = DOM.get( IDS.ta_content ); |
| 234 |
if ( ! ta_form || ! ta_cnt ) { |
| 235 |
return; |
| 236 |
} |
| 237 |
|
| 238 |
function on_change() { |
| 239 |
if ( State.raw.is_programmatic_update ) { |
| 240 |
return; |
| 241 |
} |
| 242 |
State.set_dirty( true ); |
| 243 |
} |
| 244 |
|
| 245 |
ta_form.addEventListener( 'input', on_change ); |
| 246 |
ta_form.addEventListener( 'change', on_change ); |
| 247 |
ta_cnt.addEventListener( 'input', on_change ); |
| 248 |
ta_cnt.addEventListener( 'change', on_change ); |
| 249 |
|
| 250 |
State.raw.textarea_fallback_bound = true; |
| 251 |
} |
| 252 |
|
| 253 |
function init_editor(textarea_el, key) { |
| 254 |
var wpns = w.wp || null; |
| 255 |
|
| 256 |
if ( ! textarea_el || ! wpns || ! wpns.codeEditor || typeof wpns.codeEditor.initialize !== 'function' ) { |
| 257 |
return null; |
| 258 |
} |
| 259 |
|
| 260 |
var base = w.wpbc_bfb_code_editor_settings || null; |
| 261 |
if ( ! base ) { |
| 262 |
return null; |
| 263 |
} |
| 264 |
|
| 265 |
ensure_oshortcode_mode(); |
| 266 |
|
| 267 |
// Clone so we don't mutate localized shared settings object. |
| 268 |
var settings = Object.assign( {}, base ); |
| 269 |
settings.codemirror = Object.assign( {}, base.codemirror || {} ); |
| 270 |
settings.codemirror.mode = 'oshortcode'; |
| 271 |
|
| 272 |
var inst = wpns.codeEditor.initialize( textarea_el, settings ); |
| 273 |
|
| 274 |
if ( inst && inst.codemirror ) { |
| 275 |
inst.codemirror.on( 'change', function () { |
| 276 |
if ( State.raw.is_programmatic_update ) { |
| 277 |
return; |
| 278 |
} |
| 279 |
State.set_dirty( true ); |
| 280 |
} ); |
| 281 |
} |
| 282 |
|
| 283 |
State.raw.editors[key] = inst; |
| 284 |
return inst; |
| 285 |
} |
| 286 |
|
| 287 |
function ensure_inited() { |
| 288 |
if ( State.raw.is_inited ) { |
| 289 |
return true; |
| 290 |
} |
| 291 |
|
| 292 |
var ta_form = DOM.get( IDS.ta_form ); |
| 293 |
var ta_cnt = DOM.get( IDS.ta_content ); |
| 294 |
if ( ! ta_form || ! ta_cnt ) { |
| 295 |
return false; |
| 296 |
} |
| 297 |
|
| 298 |
bind_textarea_dirty_fallback(); |
| 299 |
|
| 300 |
if ( can_init_codemirror() ) { |
| 301 |
var i1 = init_editor( ta_form, KEY.FORM ); |
| 302 |
var i2 = init_editor( ta_cnt, KEY.CONTENT ); |
| 303 |
|
| 304 |
if ( ! i1 ) { |
| 305 |
State.raw.editors[KEY.FORM] = null; |
| 306 |
} |
| 307 |
if ( ! i2 ) { |
| 308 |
State.raw.editors[KEY.CONTENT] = null; |
| 309 |
} |
| 310 |
} |
| 311 |
|
| 312 |
State.raw.is_inited = true; |
| 313 |
return true; |
| 314 |
} |
| 315 |
|
| 316 |
function refresh_all() { |
| 317 |
var keys = Object.keys( State.raw.editors ); |
| 318 |
for ( var i = 0; i < keys.length; i++ ) { |
| 319 |
var inst = State.raw.editors[keys[i]]; |
| 320 |
try { |
| 321 |
if ( inst && inst.codemirror && typeof inst.codemirror.refresh === 'function' ) { |
| 322 |
inst.codemirror.refresh(); |
| 323 |
} |
| 324 |
} catch ( e ) {} |
| 325 |
} |
| 326 |
} |
| 327 |
|
| 328 |
function textarea_id_for(key) { |
| 329 |
return TA_ID_BY_KEY[key] || ''; |
| 330 |
} |
| 331 |
|
| 332 |
function get_value(key) { |
| 333 |
var inst = State.raw.editors[key]; |
| 334 |
if ( inst && inst.codemirror && typeof inst.codemirror.getValue === 'function' ) { |
| 335 |
return String( inst.codemirror.getValue() || '' ); |
| 336 |
} |
| 337 |
var ta = DOM.get( textarea_id_for( key ) ); |
| 338 |
return ta ? String( ta.value || '' ) : ''; |
| 339 |
} |
| 340 |
|
| 341 |
function set_value(key, value) { |
| 342 |
value = (value == null) ? '' : String( value ); |
| 343 |
|
| 344 |
var ta = DOM.get( textarea_id_for( key ) ); |
| 345 |
|
| 346 |
State.raw.is_programmatic_update = true; |
| 347 |
try { |
| 348 |
if ( ta ) { |
| 349 |
ta.value = value; |
| 350 |
} |
| 351 |
|
| 352 |
var inst = State.raw.editors[key]; |
| 353 |
if ( inst && inst.codemirror && typeof inst.codemirror.setValue === 'function' ) { |
| 354 |
inst.codemirror.setValue( value ); |
| 355 |
if ( typeof inst.codemirror.save === 'function' ) { |
| 356 |
inst.codemirror.save(); |
| 357 |
} |
| 358 |
} |
| 359 |
} finally { |
| 360 |
State.raw.is_programmatic_update = false; |
| 361 |
} |
| 362 |
} |
| 363 |
|
| 364 |
function focus_and_select(key) { |
| 365 |
var ta = DOM.get( textarea_id_for( key ) ); |
| 366 |
if ( ! ta ) { |
| 367 |
return; |
| 368 |
} |
| 369 |
try { |
| 370 |
ta.focus(); |
| 371 |
ta.select(); |
| 372 |
} catch ( e ) {} |
| 373 |
} |
| 374 |
|
| 375 |
function save_all_to_textareas() { |
| 376 |
var keys = Object.keys( State.raw.editors ); |
| 377 |
for ( var i = 0; i < keys.length; i++ ) { |
| 378 |
var inst = State.raw.editors[keys[i]]; |
| 379 |
try { |
| 380 |
if ( inst && inst.codemirror && typeof inst.codemirror.save === 'function' ) { |
| 381 |
inst.codemirror.save(); |
| 382 |
} |
| 383 |
} catch ( e ) {} |
| 384 |
} |
| 385 |
} |
| 386 |
|
| 387 |
return { |
| 388 |
ensure_inited: ensure_inited, |
| 389 |
refresh_all : refresh_all, |
| 390 |
get_value : get_value, |
| 391 |
set_value : set_value, |
| 392 |
focus_select : focus_and_select, |
| 393 |
save_all : save_all_to_textareas |
| 394 |
}; |
| 395 |
})(); |
| 396 |
|
| 397 |
// == Builder export + Sync ====================================================================================== |
| 398 |
var Sync = (function () { |
| 399 |
|
| 400 |
var poll_timer_id = null; |
| 401 |
var debounce_timer_id = null; |
| 402 |
|
| 403 |
function can_export_from_builder() { |
| 404 |
return !! ( |
| 405 |
w.wpbc_bfb && |
| 406 |
typeof w.wpbc_bfb.get_structure === 'function' && |
| 407 |
w.WPBC_BFB_Exporter && |
| 408 |
typeof w.WPBC_BFB_Exporter.export_all === 'function' |
| 409 |
); |
| 410 |
} |
| 411 |
|
| 412 |
function get_current_structure() { |
| 413 |
return (w.wpbc_bfb && typeof w.wpbc_bfb.get_structure === 'function') ? w.wpbc_bfb.get_structure() : []; |
| 414 |
} |
| 415 |
|
| 416 |
function export_all_from_builder() { |
| 417 |
if ( ! w.WPBC_BFB_Exporter || typeof w.WPBC_BFB_Exporter.export_all !== 'function' ) { |
| 418 |
return null; |
| 419 |
} |
| 420 |
try { |
| 421 |
return w.WPBC_BFB_Exporter.export_all( get_current_structure(), { gapPercent: 3 } ); |
| 422 |
} catch ( e ) { |
| 423 |
return null; |
| 424 |
} |
| 425 |
} |
| 426 |
|
| 427 |
function export_output_looks_ready(out) { |
| 428 |
if ( ! out ) { |
| 429 |
return false; |
| 430 |
} |
| 431 |
|
| 432 |
var af = (out.advanced_form == null) ? '' : String( out.advanced_form ); |
| 433 |
var cf = (out.fields_data == null) ? '' : String( out.fields_data ); |
| 434 |
|
| 435 |
af = af.trim(); |
| 436 |
cf = cf.trim(); |
| 437 |
|
| 438 |
if ( ! af && ! cf ) { |
| 439 |
return false; |
| 440 |
} |
| 441 |
|
| 442 |
// If structure is loaded and there are NO fields, accept empty export. |
| 443 |
if ( structure_loaded_once && ! adapted_has_any_field( out.adapted ) ) { |
| 444 |
return true; |
| 445 |
} |
| 446 |
|
| 447 |
// Real BFB export includes layout tags (<r>, <c>, <item>). |
| 448 |
var has_layout_tags = /<\s*(r|c|item)\b/i.test( af ) || /<\s*(r|c|item)\b/i.test( cf ); |
| 449 |
var has_any_item = /<\s*item\b/i.test( af ) || /<\s*item\b/i.test( cf ); |
| 450 |
|
| 451 |
return has_layout_tags && has_any_item; |
| 452 |
} |
| 453 |
|
| 454 |
function get_builder_export_if_ready() { |
| 455 |
if ( ! can_export_from_builder() ) { |
| 456 |
return null; |
| 457 |
} |
| 458 |
|
| 459 |
var out = export_all_from_builder(); |
| 460 |
if ( ! out ) { |
| 461 |
return null; |
| 462 |
} |
| 463 |
|
| 464 |
if ( ! export_output_looks_ready( out ) ) { |
| 465 |
return null; |
| 466 |
} |
| 467 |
|
| 468 |
return out; |
| 469 |
} |
| 470 |
|
| 471 |
// --- Normalization for compare ------------------------------------------------------- |
| 472 |
function normalize_style_value(css) { |
| 473 |
css = (css == null) ? '' : String( css ); |
| 474 |
|
| 475 |
css = css.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' ); |
| 476 |
css = css.replace( /\t/g, ' ' ); |
| 477 |
css = css.replace( /\s+/g, ' ' ); |
| 478 |
|
| 479 |
css = css.replace( /\s*;\s*/g, ';' ); |
| 480 |
css = css.replace( /\s*:\s*/g, ':' ); |
| 481 |
|
| 482 |
css = css.trim().replace( /;+\s*$/g, '' ); |
| 483 |
if ( css !== '' ) { |
| 484 |
css += ';'; |
| 485 |
} |
| 486 |
|
| 487 |
return css; |
| 488 |
} |
| 489 |
|
| 490 |
function normalize_inline_styles_in_markup(html) { |
| 491 |
html = (html == null) ? '' : String( html ); |
| 492 |
|
| 493 |
return html.replace( /\bstyle=(["'])(.*?)\1/gi, function (_m, quote, css) { |
| 494 |
return 'style=' + quote + normalize_style_value( css ) + quote; |
| 495 |
} ); |
| 496 |
} |
| 497 |
|
| 498 |
function normalize_text(s) { |
| 499 |
s = (s == null) ? '' : String( s ); |
| 500 |
|
| 501 |
s = s.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' ); |
| 502 |
s = normalize_inline_styles_in_markup( s ); |
| 503 |
|
| 504 |
s = s.replace( /[ \t]+$/gm, '' ); |
| 505 |
s = s.replace( /[ ]{2,}/g, ' ' ); |
| 506 |
s = s.replace( /^\s+/gm, '' ); |
| 507 |
s = s.replace( /\n{3,}/g, '\n\n' ); |
| 508 |
|
| 509 |
return s.trim(); |
| 510 |
} |
| 511 |
|
| 512 |
function get_current_advanced_texts() { |
| 513 |
return { |
| 514 |
advanced_form: Editor.get_value( KEY.FORM ), |
| 515 |
content_form : Editor.get_value( KEY.CONTENT ) |
| 516 |
}; |
| 517 |
} |
| 518 |
|
| 519 |
function detect_sync_state_with_export() { |
| 520 |
var out = get_builder_export_if_ready(); |
| 521 |
if ( ! out ) { |
| 522 |
return null; |
| 523 |
} |
| 524 |
|
| 525 |
var cur = get_current_advanced_texts(); |
| 526 |
|
| 527 |
var a1 = normalize_text( cur.advanced_form ); |
| 528 |
var a2 = normalize_text( cur.content_form ); |
| 529 |
|
| 530 |
var b1 = normalize_text( out.advanced_form || '' ); |
| 531 |
var b2 = normalize_text( out.fields_data || '' ); |
| 532 |
|
| 533 |
return { |
| 534 |
is_synced: (a1 === b1) && (a2 === b2), |
| 535 |
out : out |
| 536 |
}; |
| 537 |
} |
| 538 |
|
| 539 |
function apply_autosync_from_sync_state(is_synced) { |
| 540 |
var cb = DOM.get( IDS.cb_autosync ); |
| 541 |
if ( ! cb ) { |
| 542 |
return; |
| 543 |
} |
| 544 |
|
| 545 |
if ( State.raw.is_dirty ) { |
| 546 |
State.raw.autosync_user_value = false; |
| 547 |
State.raw.sync_state_bootstrapped = true; |
| 548 |
cb.checked = false; |
| 549 |
State.update_badges(); |
| 550 |
return; |
| 551 |
} |
| 552 |
|
| 553 |
// Only auto-set checkbox if user never explicitly touched it. |
| 554 |
if ( State.raw.autosync_user_value === null ) { |
| 555 |
cb.checked = !! is_synced; |
| 556 |
} |
| 557 |
|
| 558 |
State.raw.sync_state_bootstrapped = true; |
| 559 |
State.update_badges(); |
| 560 |
} |
| 561 |
|
| 562 |
function regenerate_from_builder(out_opt) { |
| 563 |
var out = out_opt || get_builder_export_if_ready(); |
| 564 |
if ( ! out ) { |
| 565 |
return false; |
| 566 |
} |
| 567 |
|
| 568 |
Editor.set_value( KEY.FORM, out.advanced_form || '' ); |
| 569 |
Editor.set_value( KEY.CONTENT, out.fields_data || '' ); |
| 570 |
|
| 571 |
State.set_dirty( false ); |
| 572 |
State.update_badges(); |
| 573 |
|
| 574 |
return true; |
| 575 |
} |
| 576 |
|
| 577 |
function sync_detect_and_apply() { |
| 578 |
var res = detect_sync_state_with_export(); |
| 579 |
if ( res === null ) { |
| 580 |
return false; |
| 581 |
} |
| 582 |
|
| 583 |
var cb = DOM.get( IDS.cb_autosync ); |
| 584 |
var sync_on = !! (cb && cb.checked); |
| 585 |
|
| 586 |
State.raw.sync_state_bootstrapped = true; |
| 587 |
|
| 588 |
if ( State.raw.is_dirty ) { |
| 589 |
apply_autosync_from_sync_state( false ); |
| 590 |
return true; |
| 591 |
} |
| 592 |
|
| 593 |
if ( sync_on ) { |
| 594 |
if ( ! res.is_synced ) { |
| 595 |
regenerate_from_builder( res.out ); |
| 596 |
} else { |
| 597 |
State.update_badges(); |
| 598 |
} |
| 599 |
return true; |
| 600 |
} |
| 601 |
|
| 602 |
apply_autosync_from_sync_state( res.is_synced ); |
| 603 |
return true; |
| 604 |
} |
| 605 |
|
| 606 |
// --- Poll / debounce ------------------------------------------------------- |
| 607 |
function schedule_sync_detect(reason) { |
| 608 |
|
| 609 |
if ( poll_timer_id ) { |
| 610 |
clearTimeout( poll_timer_id ); |
| 611 |
poll_timer_id = null; |
| 612 |
} |
| 613 |
|
| 614 |
var started_ms = Date.now(); |
| 615 |
var max_total_ms = 12000; |
| 616 |
var delay_ms = 150; |
| 617 |
|
| 618 |
(function tick() { |
| 619 |
|
| 620 |
if ( ! DOM.has_ui() ) { |
| 621 |
poll_timer_id = null; |
| 622 |
return; |
| 623 |
} |
| 624 |
|
| 625 |
Editor.ensure_inited(); |
| 626 |
|
| 627 |
if ( State.raw.is_dirty ) { |
| 628 |
apply_autosync_from_sync_state( false ); |
| 629 |
poll_timer_id = null; |
| 630 |
return; |
| 631 |
} |
| 632 |
|
| 633 |
if ( sync_detect_and_apply() ) { |
| 634 |
poll_timer_id = null; |
| 635 |
return; |
| 636 |
} |
| 637 |
|
| 638 |
if ( (Date.now() - started_ms) < max_total_ms ) { |
| 639 |
delay_ms = Math.min( 600, Math.floor( delay_ms * 1.25 ) ); |
| 640 |
poll_timer_id = setTimeout( tick, delay_ms ); |
| 641 |
return; |
| 642 |
} |
| 643 |
|
| 644 |
poll_timer_id = null; |
| 645 |
})(); |
| 646 |
} |
| 647 |
|
| 648 |
function schedule_sync_detect_debounced(reason) { |
| 649 |
if ( debounce_timer_id ) { |
| 650 |
clearTimeout( debounce_timer_id ); |
| 651 |
} |
| 652 |
debounce_timer_id = setTimeout( function () { |
| 653 |
debounce_timer_id = null; |
| 654 |
schedule_sync_detect( reason ); |
| 655 |
}, 250 ); |
| 656 |
} |
| 657 |
|
| 658 |
return { |
| 659 |
schedule_detect : schedule_sync_detect, |
| 660 |
schedule_detect_debounced: schedule_sync_detect_debounced, |
| 661 |
regenerate : regenerate_from_builder, |
| 662 |
apply_autosync_state : apply_autosync_from_sync_state |
| 663 |
}; |
| 664 |
})(); |
| 665 |
|
| 666 |
// == Clipboard ================================================================================================== |
| 667 |
var Clipboard = (function () { |
| 668 |
|
| 669 |
async function copy_text(text) { |
| 670 |
text = (text == null) ? '' : String( text ); |
| 671 |
|
| 672 |
if ( typeof w.wpbc_copy_to_clipboard === 'function' ) { |
| 673 |
try { return await w.wpbc_copy_to_clipboard( text ); } catch ( e ) {} |
| 674 |
} |
| 675 |
|
| 676 |
try { |
| 677 |
if ( w.isSecureContext && navigator.clipboard && navigator.clipboard.writeText ) { |
| 678 |
await navigator.clipboard.writeText( text ); |
| 679 |
return true; |
| 680 |
} |
| 681 |
} catch ( e ) {} |
| 682 |
|
| 683 |
try { |
| 684 |
var ta = d.createElement( 'textarea' ); |
| 685 |
ta.value = text; |
| 686 |
ta.setAttribute( 'readonly', '' ); |
| 687 |
ta.style.position = 'fixed'; |
| 688 |
ta.style.top = '-9999px'; |
| 689 |
ta.style.opacity = '0'; |
| 690 |
d.body.appendChild( ta ); |
| 691 |
ta.focus(); |
| 692 |
ta.select(); |
| 693 |
var ok = d.execCommand( 'copy' ); |
| 694 |
d.body.removeChild( ta ); |
| 695 |
return !! ok; |
| 696 |
} catch ( e ) { |
| 697 |
return false; |
| 698 |
} |
| 699 |
} |
| 700 |
|
| 701 |
function feedback_button(btn, ok) { |
| 702 |
if ( ! btn ) { |
| 703 |
return; |
| 704 |
} |
| 705 |
|
| 706 |
var original = btn.getAttribute( 'data-wpbc-original-text' ) || btn.textContent; |
| 707 |
btn.setAttribute( 'data-wpbc-original-text', original ); |
| 708 |
|
| 709 |
btn.textContent = ok ? 'Copied!' : 'Press Ctrl/Cmd+C to copy'; |
| 710 |
setTimeout( function () { |
| 711 |
btn.textContent = original; |
| 712 |
}, 1500 ); |
| 713 |
} |
| 714 |
|
| 715 |
async function copy_editor_value(key, btn) { |
| 716 |
Editor.ensure_inited(); |
| 717 |
|
| 718 |
var ok = await copy_text( Editor.get_value( key ) ); |
| 719 |
if ( ! ok ) { |
| 720 |
Editor.focus_select( key ); |
| 721 |
} |
| 722 |
|
| 723 |
feedback_button( btn, ok ); |
| 724 |
} |
| 725 |
|
| 726 |
return { |
| 727 |
copy_editor_value: copy_editor_value |
| 728 |
}; |
| 729 |
})(); |
| 730 |
|
| 731 |
// == UI bindings |
| 732 |
var UI = (function () { |
| 733 |
|
| 734 |
function bind_ui() { |
| 735 |
|
| 736 |
DOM.on( DOM.get( IDS.btn_regen ), 'click', function (e) { |
| 737 |
e.preventDefault(); |
| 738 |
Editor.ensure_inited(); |
| 739 |
Sync.regenerate(); |
| 740 |
Editor.refresh_all(); |
| 741 |
|
| 742 |
// Safe default until we can confirm sync state. |
| 743 |
var cb = DOM.get( IDS.cb_autosync ); |
| 744 |
if ( cb ) { |
| 745 |
cb.checked = true; |
| 746 |
State.update_badges(); |
| 747 |
} |
| 748 |
} ); |
| 749 |
|
| 750 |
DOM.on( DOM.get( IDS.cb_autosync ), 'change', function () { |
| 751 |
|
| 752 |
var cb = DOM.get( IDS.cb_autosync ); |
| 753 |
if ( ! cb ) { |
| 754 |
return; |
| 755 |
} |
| 756 |
|
| 757 |
State.raw.autosync_user_value = !! cb.checked; // explicit user choice |
| 758 |
State.raw.sync_state_bootstrapped = true; |
| 759 |
|
| 760 |
if ( cb.checked ) { |
| 761 |
State.set_dirty( false ); |
| 762 |
Editor.ensure_inited(); |
| 763 |
|
| 764 |
if ( ! Sync.regenerate() ) { |
| 765 |
Sync.schedule_detect( 'autosync_user_on_wait_ready' ); |
| 766 |
} |
| 767 |
|
| 768 |
Editor.refresh_all(); |
| 769 |
} |
| 770 |
|
| 771 |
State.update_badges(); |
| 772 |
} ); |
| 773 |
|
| 774 |
DOM.on( DOM.get( IDS.btn_copy_form ), 'click', function (e) { |
| 775 |
e.preventDefault(); |
| 776 |
Clipboard.copy_editor_value( KEY.FORM, DOM.get( IDS.btn_copy_form ) ); |
| 777 |
} ); |
| 778 |
|
| 779 |
DOM.on( DOM.get( IDS.btn_copy_cnt ), 'click', function (e) { |
| 780 |
e.preventDefault(); |
| 781 |
Clipboard.copy_editor_value( KEY.CONTENT, DOM.get( IDS.btn_copy_cnt ) ); |
| 782 |
} ); |
| 783 |
} |
| 784 |
|
| 785 |
return { |
| 786 |
bind: bind_ui |
| 787 |
}; |
| 788 |
})(); |
| 789 |
|
| 790 |
// == WP / Builder events ======================================================================================== |
| 791 |
var Events = (function () { |
| 792 |
|
| 793 |
function hook_events() { |
| 794 |
|
| 795 |
/** |
| 796 |
* Infer apply source without relying on any "advanced_source" key. |
| 797 |
* |
| 798 |
* Returns: |
| 799 |
* - 'builder' : if texts match current Builder export (when export is ready) |
| 800 |
* - 'advanced' : if texts do not match Builder export (but have any content) |
| 801 |
* - 'auto' : if Builder export is not available/ready (safe default) |
| 802 |
* |
| 803 |
* @param {string} af |
| 804 |
* @param {string} cf |
| 805 |
* @return {'builder'|'advanced'|'auto'} |
| 806 |
*/ |
| 807 |
function infer_apply_source(af, cf) { |
| 808 |
|
| 809 |
af = (af == null) ? '' : String(af); |
| 810 |
cf = (cf == null) ? '' : String(cf); |
| 811 |
|
| 812 |
function has_text(v) { |
| 813 |
return !! (v && String(v).trim()); |
| 814 |
} |
| 815 |
|
| 816 |
function normalize_style_value(css) { |
| 817 |
css = (css == null) ? '' : String( css ); |
| 818 |
|
| 819 |
css = css.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' ); |
| 820 |
css = css.replace( /\t/g, ' ' ); |
| 821 |
css = css.replace( /\s+/g, ' ' ); |
| 822 |
|
| 823 |
// Normalize spacing around separators. |
| 824 |
css = css.replace( /\s*;\s*/g, ';' ); |
| 825 |
css = css.replace( /\s*:\s*/g, ':' ); |
| 826 |
|
| 827 |
// Ensure stable trailing semicolon (important for compare). |
| 828 |
css = css.trim().replace( /;+\s*$/g, '' ); |
| 829 |
if ( css !== '' ) { |
| 830 |
css += ';'; |
| 831 |
} |
| 832 |
return css; |
| 833 |
} |
| 834 |
|
| 835 |
function normalize_inline_styles_in_markup(html) { |
| 836 |
html = (html == null) ? '' : String( html ); |
| 837 |
|
| 838 |
// Handles: style="..." and style='...' (with optional spaces around '=') |
| 839 |
return html.replace( /\bstyle\s*=\s*(["'])(.*?)\1/gi, function (_m, quote, css) { |
| 840 |
return 'style=' + quote + normalize_style_value( css ) + quote; |
| 841 |
} ); |
| 842 |
} |
| 843 |
|
| 844 |
function normalize_text(s) { |
| 845 |
s = (s == null) ? '' : String(s); |
| 846 |
s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); |
| 847 |
|
| 848 |
// IMPORTANT: normalize inline styles so "flex:1" == "flex:1;" |
| 849 |
s = normalize_inline_styles_in_markup(s); |
| 850 |
|
| 851 |
s = s.replace(/[ \t]+$/gm, ''); |
| 852 |
s = s.replace(/[ ]{2,}/g, ' '); |
| 853 |
s = s.replace(/^\s+/gm, ''); |
| 854 |
s = s.replace(/\n{3,}/g, '\n\n'); |
| 855 |
return s.trim(); |
| 856 |
} |
| 857 |
|
| 858 |
function export_output_looks_ready(out) { |
| 859 |
if (!out) return false; |
| 860 |
var a = normalize_text(out.advanced_form || ''); |
| 861 |
var b = normalize_text(out.fields_data || ''); |
| 862 |
if (!a && !b) return false; |
| 863 |
// If structure is loaded and there are NO fields, accept empty export. |
| 864 |
if ( structure_loaded_once && ! adapted_has_any_field( out.adapted ) ) { |
| 865 |
return true; |
| 866 |
} |
| 867 |
// Expect layout tags to exist in real export. |
| 868 |
var has_layout_tags = /<\s*(r|c|item)\b/i.test(a) || /<\s*(r|c|item)\b/i.test(b); |
| 869 |
var has_any_item = /<\s*item\b/i.test(a) || /<\s*item\b/i.test(b); |
| 870 |
return has_layout_tags && has_any_item; |
| 871 |
} |
| 872 |
|
| 873 |
function try_get_builder_export() { |
| 874 |
try { |
| 875 |
if ( |
| 876 |
!w.wpbc_bfb || |
| 877 |
typeof w.wpbc_bfb.get_structure !== 'function' || |
| 878 |
!w.WPBC_BFB_Exporter || |
| 879 |
typeof w.WPBC_BFB_Exporter.export_all !== 'function' |
| 880 |
) { |
| 881 |
return null; |
| 882 |
} |
| 883 |
var out = w.WPBC_BFB_Exporter.export_all(w.wpbc_bfb.get_structure(), { gapPercent: 3 }); |
| 884 |
if (!export_output_looks_ready(out)) { |
| 885 |
return null; |
| 886 |
} |
| 887 |
return out; |
| 888 |
} catch (_e) { |
| 889 |
return null; |
| 890 |
} |
| 891 |
} |
| 892 |
|
| 893 |
var out = try_get_builder_export(); |
| 894 |
if (!out) { |
| 895 |
return 'auto'; |
| 896 |
} |
| 897 |
|
| 898 |
var in_af = normalize_text(af); |
| 899 |
var in_cf = normalize_text(cf); |
| 900 |
var ex_af = normalize_text(out.advanced_form || ''); |
| 901 |
var ex_cf = normalize_text(out.fields_data || ''); |
| 902 |
|
| 903 |
if (in_af === ex_af && in_cf === ex_cf) { |
| 904 |
return 'builder'; |
| 905 |
} |
| 906 |
|
| 907 |
if (has_text(af) || has_text(cf)) { |
| 908 |
return 'advanced'; |
| 909 |
} |
| 910 |
|
| 911 |
return 'auto'; |
| 912 |
} |
| 913 |
|
| 914 |
|
| 915 |
d.addEventListener( 'wpbc:bfb:structure:change', function () { |
| 916 |
if ( ! Editor.ensure_inited() ) { |
| 917 |
return; |
| 918 |
} |
| 919 |
|
| 920 |
if ( State.raw.is_dirty ) { |
| 921 |
return; |
| 922 |
} |
| 923 |
|
| 924 |
if ( State.raw.autosync_user_value === false ) { |
| 925 |
return; |
| 926 |
} |
| 927 |
|
| 928 |
var cb = DOM.get( IDS.cb_autosync ); |
| 929 |
if ( ! cb ) { |
| 930 |
return; |
| 931 |
} |
| 932 |
|
| 933 |
if ( ! State.raw.sync_state_bootstrapped ) { |
| 934 |
Sync.schedule_detect_debounced( 'structure_change_pre_bootstrap' ); |
| 935 |
return; |
| 936 |
} |
| 937 |
|
| 938 |
Sync.schedule_detect_debounced( 'structure_change' ); |
| 939 |
} ); |
| 940 |
|
| 941 |
d.addEventListener( 'wpbc:bfb:structure:loaded', function () { |
| 942 |
structure_loaded_once = true; |
| 943 |
Sync.schedule_detect( 'structure_loaded' ); |
| 944 |
} ); |
| 945 |
|
| 946 |
d.addEventListener( 'wpbc:bfb:top-tab', function (ev) { |
| 947 |
|
| 948 |
var tab_id = (ev && ev.detail && ev.detail.tab) ? String( ev.detail.tab ) : ''; |
| 949 |
var is_inner = (tab_id === 'advanced_mode__booking_form' || tab_id === 'advanced_mode__booking_data'); |
| 950 |
|
| 951 |
// Entering Advanced Mode (root tab) or switching inner tabs. |
| 952 |
if ( tab_id !== 'advanced_tab' && ! is_inner ) { |
| 953 |
return; |
| 954 |
} |
| 955 |
|
| 956 |
if ( ! Editor.ensure_inited() ) { |
| 957 |
return; |
| 958 |
} |
| 959 |
|
| 960 |
if ( State.is_autosync_on() ) { |
| 961 |
if ( ! Sync.regenerate() ) { |
| 962 |
Sync.schedule_detect( 'top_tab_wait_ready' ); |
| 963 |
} |
| 964 |
} |
| 965 |
|
| 966 |
|
| 967 |
setTimeout( Editor.refresh_all, 60 ); |
| 968 |
if ( tab_id === 'advanced_mode__booking_data' ) { |
| 969 |
setTimeout( Editor.refresh_all, 120 ); |
| 970 |
} |
| 971 |
} ); |
| 972 |
|
| 973 |
d.addEventListener( 'wpbc:bfb:advanced_text:apply', function (ev) { |
| 974 |
|
| 975 |
var det = (ev && ev.detail) ? ev.detail : {}; |
| 976 |
var af = (det.advanced_form == null) ? '' : String( det.advanced_form ); |
| 977 |
var cf = (det.content_form == null) ? '' : String( det.content_form ); |
| 978 |
|
| 979 |
var src = String( det.advanced_mode_source || 'auto' ).toLowerCase(); // builder|advanced|auto. |
| 980 |
if ( src !== 'builder' && src !== 'advanced' && src !== 'auto' ) { |
| 981 |
src = 'auto'; |
| 982 |
} |
| 983 |
if ( src === 'auto' ) { |
| 984 |
src = infer_apply_source( af, cf ); // builder|advanced|auto. |
| 985 |
} |
| 986 |
|
| 987 |
|
| 988 |
var cb = DOM.get( IDS.cb_autosync ); |
| 989 |
|
| 990 |
// Always use Editor API (keeps textarea + CodeMirror in sync). |
| 991 |
Editor.ensure_inited(); |
| 992 |
Editor.set_value( KEY.FORM, af ); |
| 993 |
Editor.set_value( KEY.CONTENT, cf ); |
| 994 |
setTimeout( Editor.refresh_all, 60 ); |
| 995 |
|
| 996 |
if ( src === 'advanced' ) { |
| 997 |
// Advanced is authoritative => dirty + autosync OFF. |
| 998 |
if ( cb ) { cb.checked = false; } |
| 999 |
State.set_dirty( true ); // also sets autosync_user_value=false and bootstrapped=true . |
| 1000 |
|
| 1001 |
} else if ( src === 'builder' ) { |
| 1002 |
// Builder is authoritative => autosync ON. |
| 1003 |
if ( cb ) { cb.checked = true; } |
| 1004 |
State.raw.autosync_user_value = true; |
| 1005 |
State.raw.sync_state_bootstrapped = true; |
| 1006 |
State.set_dirty( false ); |
| 1007 |
|
| 1008 |
} else { |
| 1009 |
// auto/unknown: Safe default = autosync OFF until we confirm real sync state. This prevents Preview using Builder before export is ready / comparison is done. |
| 1010 |
if ( cb ) { cb.checked = false; } |
| 1011 |
State.raw.autosync_user_value = null; |
| 1012 |
State.raw.sync_state_bootstrapped = false; |
| 1013 |
State.set_dirty( false ); |
| 1014 |
} |
| 1015 |
|
| 1016 |
State.update_badges(); |
| 1017 |
Sync.schedule_detect( 'advanced_text_apply' ); |
| 1018 |
} ); |
| 1019 |
|
| 1020 |
|
| 1021 |
} |
| 1022 |
|
| 1023 |
return { |
| 1024 |
hook: hook_events |
| 1025 |
}; |
| 1026 |
})(); |
| 1027 |
|
| 1028 |
// == Public API (unchanged) ====================================================================================== |
| 1029 |
w.wpbc_bfb_advanced_editor_api = w.wpbc_bfb_advanced_editor_api || {}; |
| 1030 |
|
| 1031 |
w.wpbc_bfb_advanced_editor_api.get_values = function () { |
| 1032 |
|
| 1033 |
// Ensure CodeMirror is ready (if enabled). |
| 1034 |
Editor.ensure_inited(); |
| 1035 |
|
| 1036 |
// Push CodeMirror -> textarea (no-op if not inited). |
| 1037 |
Editor.save_all(); |
| 1038 |
|
| 1039 |
// In your system, "manual mode" (autosync OFF) must be treated as "use Advanced". |
| 1040 |
var use_advanced = !! State.raw.is_dirty || ! State.is_autosync_on(); |
| 1041 |
|
| 1042 |
return { |
| 1043 |
advanced_form: Editor.get_value( KEY.FORM ), |
| 1044 |
content_form : Editor.get_value( KEY.CONTENT ), |
| 1045 |
is_dirty : use_advanced |
| 1046 |
}; |
| 1047 |
}; |
| 1048 |
|
| 1049 |
|
| 1050 |
w.wpbc_bfb_advanced_editor_api.set_dirty = function (state) { |
| 1051 |
State.set_dirty( !! state ); |
| 1052 |
}; |
| 1053 |
|
| 1054 |
// == Boot ======================================================================================================= |
| 1055 |
d.addEventListener( 'DOMContentLoaded', function () { |
| 1056 |
|
| 1057 |
UI.bind(); |
| 1058 |
Events.hook(); |
| 1059 |
|
| 1060 |
if ( DOM.has_ui() ) { |
| 1061 |
Editor.ensure_inited(); |
| 1062 |
setTimeout( Editor.refresh_all, 60 ); |
| 1063 |
|
| 1064 |
// Safe default until we can confirm sync state. |
| 1065 |
var cb = DOM.get( IDS.cb_autosync ); |
| 1066 |
if ( cb && State.raw.autosync_user_value === null && ! State.raw.sync_state_bootstrapped ) { |
| 1067 |
cb.checked = false; |
| 1068 |
State.update_badges(); |
| 1069 |
} |
| 1070 |
} |
| 1071 |
|
| 1072 |
// Initial autosync checkbox state (based on real sync status). |
| 1073 |
Sync.schedule_detect( 'boot' ); |
| 1074 |
} ); |
| 1075 |
|
| 1076 |
})( window, document ); |
| 1077 |
|