PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-form-builder / advanced-mode / _src / bfb-advanced-free.js

bfb-advanced-free.js in Booking Calendar 11.9, at includes/page-form-builder/advanced-mode/_src/bfb-advanced-free.js

421 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*
2 * Advanced Mode Bridge (FREE) + CodeMirror Highlighting:
3 * - Builder is the only editor.
4 * - Always sync ON (Advanced text = Builder export).
5 * - CodeMirror read-only highlighting for textareas (oshortcode overlay).
6 * - Provides wpbc_bfb_advanced_editor_api.get_values() for Save/Preview.
7 *
8 * @file ../includes/page-form-builder/advanced-mode/_out/bfb-advanced-free.js
9 */
10 (function (w, d) {
11 'use strict';
12
13 // True after wpbc:bfb:structure:loaded fired at least once.
14 var structure_loaded_once = false;
15
16 // == Constants =========================================================================================
17 var IDS = {
18 ta_form : 'wpbc_bfb__advanced_form_editor',
19 ta_content: 'wpbc_bfb__content_form_editor'
20 };
21
22 var KEY = {
23 FORM : 'advanced_form',
24 CONTENT: 'content_form'
25 };
26
27 var TA_ID_BY_KEY = {};
28 TA_ID_BY_KEY[KEY.FORM] = IDS.ta_form;
29 TA_ID_BY_KEY[KEY.CONTENT] = IDS.ta_content;
30
31 // == DOM ===============================================================================================
32 var DOM = (function () {
33
34 function get_by_id(id) {
35 return d.getElementById( id );
36 }
37
38 function has_textareas() {
39 return !! (get_by_id( IDS.ta_form ) && get_by_id( IDS.ta_content ));
40 }
41
42 function on(type, fn) {
43 d.addEventListener( type, fn );
44 }
45
46 return {
47 get : get_by_id,
48 has_textareas: has_textareas,
49 on : on
50 };
51 })();
52
53 // == Editor (CodeMirror readonly + oshortcode highlighting) ============================================
54 var Editor = (function () {
55
56 var is_inited = false;
57 var editors = {}; // key -> wp.codeEditor instance (or null)
58 editors[KEY.FORM] = null;
59 editors[KEY.CONTENT] = null;
60
61 var is_oshortcode_defined = false;
62
63 function can_init_codemirror() {
64 var wpns = w.wp || null;
65 return !! (
66 wpns &&
67 wpns.codeEditor &&
68 typeof wpns.codeEditor.initialize === 'function' &&
69 w.wpbc_bfb_code_editor_settings
70 );
71 }
72
73 function ensure_oshortcode_mode() {
74 if ( is_oshortcode_defined ) {
75 return;
76 }
77
78 var CM = (w.wp && w.wp.CodeMirror) ? w.wp.CodeMirror : null;
79 if ( ! CM || typeof CM.defineMode !== 'function' ) {
80 return;
81 }
82
83 CM.defineMode( 'oshortcode', function (config, parserConfig) {
84
85 var overlay = {
86 token: function (stream) {
87 var ch;
88
89 // [name ...] or [name* ...]
90 if ( stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/ ) ) {
91 while ( (ch = stream.next()) != null ) {
92 if ( ch === ']' ) {
93 return 'oshortcode';
94 }
95 }
96 }
97
98 while ( stream.next() != null && ! stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/, false ) ) {}
99 return null;
100 }
101 };
102
103 var base = CM.getMode( config, (parserConfig && parserConfig.backdrop) || 'htmlmixed' );
104
105 // Fallback if overlay addon is missing.
106 if ( typeof CM.overlayMode !== 'function' ) {
107 return base;
108 }
109
110 return CM.overlayMode( base, overlay );
111 } );
112
113 is_oshortcode_defined = true;
114 }
115
116 function init_one(textarea_el, key) {
117 var wpns = w.wp || null;
118 if ( ! textarea_el || ! wpns || ! wpns.codeEditor || typeof wpns.codeEditor.initialize !== 'function' ) {
119 return null;
120 }
121
122 var base = w.wpbc_bfb_code_editor_settings || null;
123 if ( ! base ) {
124 return null;
125 }
126
127 ensure_oshortcode_mode();
128
129 // Clone so we don't mutate localized shared settings object.
130 var settings = Object.assign( {}, base );
131 settings.codemirror = Object.assign( {}, base.codemirror || {} );
132
133 // Highlight shortcodes in HTML-like markup.
134 settings.codemirror.mode = 'oshortcode';
135
136 // FREE: read-only editor (still selectable/copyable).
137 settings.codemirror.readOnly = true;
138
139 // Nice UX for read-only:
140 // - no active line highlight in some themes
141 // - no cursor blinking distraction
142 settings.codemirror.styleActiveLine = false;
143 settings.codemirror.cursorBlinkRate = -1;
144
145 var inst = wpns.codeEditor.initialize( textarea_el, settings );
146 editors[key] = inst || null;
147
148 return inst || null;
149 }
150
151 function ensure_inited() {
152 if ( is_inited ) {
153 return true;
154 }
155
156 var ta_form = DOM.get( IDS.ta_form );
157 var ta_cnt = DOM.get( IDS.ta_content );
158
159 if ( ! ta_form || ! ta_cnt ) {
160 return false;
161 }
162
163 // Initialize CodeMirror if possible; otherwise, plain textarea fallback.
164 if ( can_init_codemirror() ) {
165 init_one( ta_form, KEY.FORM );
166 init_one( ta_cnt, KEY.CONTENT );
167 }
168
169 is_inited = true;
170 return true;
171 }
172
173 function textarea_id_for(key) {
174 return TA_ID_BY_KEY[key] || '';
175 }
176
177 function set_value(key, value) {
178 value = (value == null) ? '' : String( value );
179
180 var ta = DOM.get( textarea_id_for( key ) );
181 if ( ta ) {
182 ta.value = value;
183 }
184
185 var inst = editors[key];
186 try {
187 if ( inst && inst.codemirror && typeof inst.codemirror.setValue === 'function' ) {
188 inst.codemirror.setValue( value );
189 if ( typeof inst.codemirror.save === 'function' ) {
190 inst.codemirror.save();
191 }
192 }
193 } catch ( e ) {}
194 }
195
196 function refresh_all() {
197 var keys = Object.keys( editors );
198 for ( var i = 0; i < keys.length; i++ ) {
199 var inst = editors[keys[i]];
200 try {
201 if ( inst && inst.codemirror && typeof inst.codemirror.refresh === 'function' ) {
202 inst.codemirror.refresh();
203 }
204 } catch ( e ) {}
205 }
206
207 var root = d.documentElement;
208 if ( ! root ) {
209 return;
210 }
211 root.setAttribute( 'data-wpbc-bfb-live-source', 'builder' );
212 }
213
214 return {
215 ensure_inited: ensure_inited,
216 set_value : set_value,
217 refresh_all : refresh_all
218 };
219 })();
220
221 // == Bridge (Builder export -> snapshot -> editor/textarea) ============================================
222 var Bridge = (function () {
223
224 var last_good = { advanced_form: '', content_form: '' };
225 var debounce_id = null;
226
227 /**
228 * Check if adapted structure contains ANY field (including inside sections).
229 *
230 * @param {Object|null} adapted
231 * @return {boolean}
232 */
233 function adapted_has_any_field(adapted) {
234 if ( ! adapted || ! Array.isArray( adapted.pages ) ) {
235 return false;
236 }
237
238 var has = false;
239
240 function walk_section(sec) {
241 if ( has || ! sec ) {
242 return;
243 }
244 var cols = Array.isArray( sec.columns ) ? sec.columns : [];
245 for ( var i = 0; i < cols.length; i++ ) {
246 var col = cols[i] || {};
247 if ( Array.isArray( col.fields ) && col.fields.length ) {
248 has = true;
249 return;
250 }
251 var nested = Array.isArray( col.sections ) ? col.sections : [];
252 for ( var j = 0; j < nested.length; j++ ) {
253 walk_section( nested[j] );
254 if ( has ) {
255 return;
256 }
257 }
258 }
259 }
260
261 for ( var p = 0; p < adapted.pages.length; p++ ) {
262 var page = adapted.pages[p] || {};
263 var items = Array.isArray( page.items ) ? page.items : [];
264 for ( var k = 0; k < items.length; k++ ) {
265 var it = items[k] || {};
266 if ( it.kind === 'field' ) {
267 return true;
268 }
269 if ( it.kind === 'section' ) {
270 walk_section( it.data );
271 if ( has ) {
272 return true;
273 }
274 }
275 }
276 }
277
278 return has;
279 }
280
281 function can_export_from_builder() {
282 return !! (
283 w.wpbc_bfb &&
284 typeof w.wpbc_bfb.get_structure === 'function' &&
285 w.WPBC_BFB_Exporter &&
286 typeof w.WPBC_BFB_Exporter.export_all === 'function'
287 );
288 }
289
290 function export_all_from_builder() {
291 if ( ! can_export_from_builder() ) {
292 return null;
293 }
294 try {
295 return w.WPBC_BFB_Exporter.export_all( w.wpbc_bfb.get_structure(), { gapPercent: 3 } );
296 } catch ( e ) {
297 return null;
298 }
299 }
300
301 // Prevent “wrapper-only / empty” early exports.
302 function export_output_looks_ready(out) {
303 if ( ! out ) {
304 return false;
305 }
306
307 var af = (out.advanced_form == null) ? '' : String( out.advanced_form ).trim();
308 var cf = (out.fields_data == null) ? '' : String( out.fields_data ).trim();
309 if ( ! af && ! cf ) {
310 return false;
311 }
312
313 // If structure is loaded and there are NO fields anywhere, accept empty export.
314 // (Empty form should still regenerate to a valid wrapper+step markup.)
315 if ( structure_loaded_once && ! adapted_has_any_field( out.adapted ) ) {
316 return true;
317 }
318
319 // Real export usually includes <item> at least.
320 var has_any_item = /<\s*item\b/i.test( af ) || /<\s*item\b/i.test( cf );
321 return has_any_item;
322 }
323
324 function apply_snapshot(values) {
325 // Update both textarea and CodeMirror (if inited).
326 Editor.ensure_inited();
327 Editor.set_value( KEY.FORM, values.advanced_form || '' );
328 Editor.set_value( KEY.CONTENT, values.content_form || '' );
329 }
330
331 function refresh_snapshot() {
332 var out = export_all_from_builder();
333 if ( ! out || ! export_output_looks_ready( out ) ) {
334 return false;
335 }
336
337 last_good.advanced_form = String( out.advanced_form || '' );
338 last_good.content_form = String( out.fields_data || '' );
339
340 apply_snapshot( last_good );
341 return true;
342 }
343
344 function schedule_refresh(ms) {
345 if ( debounce_id ) {
346 clearTimeout( debounce_id );
347 }
348 debounce_id = setTimeout( function () {
349 debounce_id = null;
350 refresh_snapshot();
351 }, ms );
352 }
353
354 function get_values() {
355 // Try to refresh now; fallback to last_good if exporter not ready.
356 refresh_snapshot();
357 return {
358 advanced_form: String( last_good.advanced_form || '' ),
359 content_form : String( last_good.content_form || '' ),
360 is_dirty : false
361 };
362 }
363
364 return {
365 refresh_now : refresh_snapshot,
366 schedule_refresh: schedule_refresh,
367 get_values : get_values
368 };
369 })();
370
371 // == Events ============================================================================================
372 var Events = (function () {
373
374 function hook() {
375
376 // Structure loaded: immediate + delayed refresh (exporter can be late).
377 DOM.on( 'wpbc:bfb:structure:loaded', function () {
378 structure_loaded_once = true;
379 Bridge.refresh_now();
380 Bridge.schedule_refresh( 200 );
381 setTimeout( Editor.refresh_all, 60 );
382 } );
383
384 // Structure changed: debounce refresh.
385 DOM.on( 'wpbc:bfb:structure:change', function () {
386 Bridge.schedule_refresh( 250 );
387 } );
388
389 // If you still have an “advanced” tab/panel visible in Free, CodeMirror may need refresh.
390 DOM.on( 'wpbc:bfb:top-tab', function () {
391 setTimeout( Editor.refresh_all, 60 );
392 } );
393 }
394
395 return { hook: hook };
396 })();
397
398 // == Public API (same as Pro) ===========================================================================
399 w.wpbc_bfb_advanced_editor_api = w.wpbc_bfb_advanced_editor_api || {};
400
401 w.wpbc_bfb_advanced_editor_api.get_values = function () {
402 return Bridge.get_values();
403 };
404
405 // Compatibility no-op.
406 w.wpbc_bfb_advanced_editor_api.set_dirty = function () {
407 };
408
409 // == Boot ==============================================================================================
410 DOM.on( 'DOMContentLoaded', function () {
411 // Init editor if textareas exist.
412 if ( DOM.has_textareas() ) {
413 Editor.ensure_inited();
414 Bridge.refresh_now();
415 setTimeout( Editor.refresh_all, 60 );
416 }
417
418 Events.hook();
419 } );
420 })( window, document );
421