PluginProbe
Booking Calendar / 11.0
Booking Calendar v11.0
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 10.11 10.11.2 All 203 releases
booking / includes / save-load-option / _src / save-load-option.js

save-load-option.js in Booking Calendar 11.0, at includes/save-load-option/_src/save-load-option.js

562 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * General Option Loader/Saver (client)
3 *
4 * - Provides:
5 * window.wpbc_save_option_from_element(el)
6 * window.wpbc_load_option_from_element(el)
7 * - Busy UI (spinner + disabled)
8 * - JSON path: send raw JSON string untouched.
9 * - RAW scalar path: send as-is.
10 * - Fields path: serialize to query-string via jQuery.param.
11 *
12 * IMPORTANT:
13 * - jQuery .data() is cached. If some code updates data-* attributes via setAttribute(),
14 * reading via $el.data(...) can return stale values.
15 * - Therefore, this module prefers reading via $el.attr('data-...') for dynamic keys
16 * (value/value-json), and falls back to $el.data(...) when attribute is missing.
17 *
18 * file: ../includes/save-load-option/_out/save-load-option.js
19 *
20 * Events:
21 * $(document).on('wpbc:option:beforeSave', (e, $el, payload) => {})
22 * $(document).on('wpbc:option:afterSave', (e, response) => {})
23 * $(document).on('wpbc:option:beforeLoad', (e, $el, name) => {})
24 * $(document).on('wpbc:option:afterLoad', (e, response) => {})
25 */
26 (function (w, $) {
27 'use strict';
28
29 /**
30 * Escape for safe HTML injection (small helper).
31 *
32 * @param {string} s
33 * @returns {string}
34 */
35 function wpbc_uix_escape_html(s) {
36 return String(s)
37 .replace(/&/g, '&')
38 .replace(/</g, '&lt;')
39 .replace(/>/g, '&gt;')
40 .replace(/"/g, '&quot;')
41 .replace(/'/g, '&#039;');
42 }
43
44 /**
45 * Read a value from data-* attribute first (fresh), then fallback to jQuery .data() cache.
46 *
47 * @param {jQuery} $el
48 * @param {string} attr_name Example: 'data-wpbc-u-save-value'
49 * @param {string} data_key Example: 'wpbc-u-save-value'
50 * @returns {*}
51 */
52 function wpbc_uix_read_attr_or_data($el, attr_name, data_key) {
53 var v = $el.attr(attr_name);
54 if (typeof v !== 'undefined') {
55 return v;
56 }
57 return $el.data(data_key);
58 }
59
60 /**
61 * Turn "On"/"Off" like values into consistent "On"/"Off".
62 * (Used for checkbox/toggle serialization.)
63 *
64 * @param {*} v
65 * @returns {string}
66 */
67 function wpbc_uix_to_on_off(v) {
68 if (v === true) { return 'On'; }
69 if (v === false) { return 'Off'; }
70 var s = String(v || '').toLowerCase();
71 if (s === 'on' || s === '1' || s === 'true' || s === 'yes') { return 'On'; }
72 return 'Off';
73 }
74
75 /**
76 * Get a useful value from an input/select/textarea element.
77 * - checkbox => 'On'/'Off'
78 * - radio => value of checked in group (if possible), else ''
79 * - others => .val()
80 *
81 * @param {jQuery} $control
82 * @returns {string}
83 */
84 function wpbc_uix_get_control_value($control) {
85
86 if (!$control || !$control.length) {
87 return '';
88 }
89
90 // checkbox/toggle.
91 if ($control.is(':checkbox')) {
92 return $control.is(':checked') ? 'On' : 'Off';
93 }
94
95 // radio group.
96 if ($control.is(':radio')) {
97 var name = $control.attr('name');
98 if (name) {
99 var $checked = $('input[type="radio"][name="' + name + '"]:checked');
100 return $checked.length ? String($checked.val()) : '';
101 }
102 return $control.is(':checked') ? String($control.val()) : '';
103 }
104
105 // select/text/textarea/etc.
106 return String($control.val() == null ? '' : $control.val());
107 }
108
109 /**
110 * Busy ON UI for a clickable element.
111 *
112 * @param {jQuery} $el
113 * @returns {void}
114 */
115 function wpbc_uix_busy_on($el) {
116 if (!$el || !$el.length || $el.data('wpbc-uix-busy')) {
117 return;
118 }
119
120 $el.data('wpbc-uix-busy', 1);
121 $el.data('wpbc-uix-original-html', $el.html());
122
123 var busy_text = $el.data('wpbc-u-busy-text');
124 var spinner = '<span class="wpbc_icn_rotate_right wpbc_spin wpbc_ajax_icon wpbc_processing wpbc_icn_autorenew" aria-hidden="true"></span>';
125
126 if (typeof busy_text === 'string' && busy_text.length) {
127 $el.html(wpbc_uix_escape_html(busy_text) + ' ' + spinner);
128 } else {
129 $el.append(spinner);
130 }
131
132 $el.addClass('wpbc-is-busy')
133 .attr('aria-disabled', 'true')
134 .prop('disabled', true);
135 }
136
137 /**
138 * Busy OFF UI for a clickable element.
139 *
140 * @param {jQuery} $el
141 * @returns {void}
142 */
143 function wpbc_uix_busy_off($el) {
144 if (!$el || !$el.length || !$el.data('wpbc-uix-busy')) {
145 return;
146 }
147
148 var original = $el.data('wpbc-uix-original-html');
149 if (typeof original === 'string') {
150 $el.html(original);
151 }
152
153 $el.removeClass('wpbc-is-busy')
154 .removeAttr('aria-disabled')
155 .prop('disabled', false);
156
157 $el.removeData('wpbc-uix-busy')
158 .removeData('wpbc-uix-original-html');
159 }
160
161 var wpbc_uix_autosave_registry = {};
162
163 function wpbc_uix_get_option_save_config($el) {
164 return {
165 nonce : $el.data('wpbc-u-save-nonce'),
166 nonce_action : $el.data('wpbc-u-save-action'),
167 data_name : $el.data('wpbc-u-save-name'),
168 fields_raw : $el.data('wpbc-u-save-fields') || '',
169 inline_value : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value'),
170 json : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json'),
171 save_mode : $el.data('wpbc-u-save-mode') || $el.attr('data-wpbc-u-save-mode') || '',
172 value_from_selector: $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from')
173 };
174 }
175
176 function wpbc_uix_build_option_save_payload($el, cfg) {
177 cfg = cfg || wpbc_uix_get_option_save_config($el);
178
179 if (typeof cfg.json === 'string' && cfg.json.trim() !== '') {
180 return cfg.json.trim();
181 }
182
183 if (cfg.value_from_selector) {
184 var $src = $(cfg.value_from_selector);
185 var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first();
186 return wpbc_uix_get_control_value($control);
187 }
188
189 if (typeof cfg.inline_value !== 'undefined' && cfg.inline_value !== null) {
190 return String(cfg.inline_value);
191 }
192
193 if (cfg.fields_raw) {
194 var fields = String(cfg.fields_raw).split(',')
195 .map(function (s) { return String(s || '').trim(); })
196 .filter(Boolean);
197
198 var data = {};
199
200 fields.forEach(function (sel) {
201 var $f = $(sel);
202 if (!$f.length) { return; }
203
204 var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first();
205 if (!$control.length) { return; }
206
207 var key = $control.attr('name') || $control.attr('id');
208 if (!key) { return; }
209
210 data[key] = wpbc_uix_get_control_value($control);
211 });
212
213 return $.param(data);
214 }
215
216 return null;
217 }
218
219 function wpbc_uix_update_autosave_registry_from_element(el, is_dirty) {
220 var $el = $(el);
221 var cfg = wpbc_uix_get_option_save_config($el);
222
223 if (!cfg.data_name) {
224 return;
225 }
226
227 wpbc_uix_autosave_registry[cfg.data_name] = {
228 nonce : cfg.nonce,
229 nonce_action: cfg.nonce_action,
230 data_name : cfg.data_name,
231 payload : wpbc_uix_build_option_save_payload($el, cfg),
232 save_mode : cfg.save_mode,
233 fields_raw : cfg.fields_raw,
234 dirty : !!is_dirty,
235 el : el
236 };
237 }
238
239 function wpbc_uix_save_autosave_registry_entry(entry) {
240 if (!entry || !entry.dirty || !entry.nonce || !entry.nonce_action || !entry.data_name || entry.payload === null) {
241 return;
242 }
243
244 $(document).trigger('wpbc:option:beforeSave', [ $(), entry.payload ]);
245
246 $.ajax({
247 url: w.wpbc_option_saver_loader_config.ajax_url,
248 type: 'POST',
249 data: {
250 action: w.wpbc_option_saver_loader_config.action_save,
251 nonce: entry.nonce,
252 nonce_action: entry.nonce_action,
253 data_name: entry.data_name,
254 data_value: entry.payload,
255 data_mode: entry.save_mode,
256 data_fields: (entry.save_mode === 'split' ? String(entry.fields_raw || '') : '')
257 }
258 })
259 .done(function (resp) {
260 if (resp && resp.success) {
261 entry.dirty = false;
262 if (entry.el) {
263 $(entry.el).attr('data-wpbc-u-autosave-dirty', '0');
264 }
265 if (typeof w.wpbc_admin_show_message === 'function') {
266 w.wpbc_admin_show_message((resp.data && resp.data.message) ? resp.data.message : 'Saved', 'success', 1000, false);
267 }
268 } else if (typeof w.wpbc_admin_show_message === 'function') {
269 w.wpbc_admin_show_message((resp && resp.data && resp.data.message) ? resp.data.message : 'Save error', 'error', 30000);
270 }
271
272 $(document).trigger('wpbc:option:afterSave', [ resp ]);
273 })
274 .fail(function (xhr) {
275 if (typeof w.wpbc_admin_show_message === 'function') {
276 w.wpbc_admin_show_message('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText, 'error', 30000);
277 }
278 $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]);
279 });
280 }
281
282 /**
283 * Save Option - send ajax request to save data.
284 *
285 * Data attributes:
286 * data-wpbc-u-save-name — option key (required)
287 * data-wpbc-u-save-nonce — nonce value (required)
288 * data-wpbc-u-save-action — nonce action (required)
289 * data-wpbc-u-save-value — RAW scalar to save (optional) (dynamic: read via attr first)
290 * data-wpbc-u-save-value-json — JSON string to save (optional) (dynamic: read via attr first)
291 * data-wpbc-u-save-fields — CSV selectors; values serialized with jQuery.param (optional). Optional allowlist of keys for split mode (CSV).
292 * data-wpbc-u-save-mode - Optional.: 'split' | '' -- Optional: split JSON object into separate options server-side.
293 * data-wpbc-u-save-value-from — OPTIONAL selector to read scalar from (checkbox => On/Off)
294 * data-wpbc-u-busy-text — custom text during AJAX (optional)
295 * data-wpbc-u-save-callback — window function name to call on success (optional)
296 *
297 * @param {HTMLElement} el element with data attributes.
298 * @returns {void}
299 */
300 w.wpbc_save_option_from_element = function (el) {
301
302 if (!w.wpbc_option_saver_loader_config) {
303 console.error('WPBC | config missing');
304 return;
305 }
306
307 var $el = $(el);
308
309 // Static values can be read from .data().
310 var nonce = $el.data('wpbc-u-save-nonce');
311 var nonce_action = $el.data('wpbc-u-save-action');
312 var data_name = $el.data('wpbc-u-save-name');
313
314 // Dynamic values MUST prefer attribute read (fresh), fallback to .data().
315 var fields_raw = $el.data('wpbc-u-save-fields') || '';
316 var inline_value = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value');
317 var json = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json');
318 var save_mode = $el.data('wpbc-u-save-mode') || $el.attr('data-wpbc-u-save-mode') || '';
319
320 // Optional: compute scalar from another control selector at click time.
321 var value_from_selector = $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from');
322
323 var cb_id = $el.data('wpbc-u-save-callback');
324 var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null;
325
326 if (!nonce || !nonce_action || !data_name) {
327 console.error('WPBC | missing nonce/action/name');
328 return;
329 }
330
331 var payload = '';
332
333 // 1) JSON path.
334 if (typeof json === 'string' && json.trim() !== '') {
335 payload = json.trim();
336 }
337 // 2) Scalar computed from selector (checkbox => On/Off).
338 else if (value_from_selector) {
339 var $src = $(value_from_selector);
340 var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first();
341 payload = wpbc_uix_get_control_value($control);
342 }
343 // 3) RAW scalar path.
344 else if (typeof inline_value !== 'undefined' && inline_value !== null) {
345 payload = String(inline_value);
346 }
347 // 4) Fields path (query-string).
348 else if (fields_raw) {
349
350 var fields = String(fields_raw).split(',')
351 .map(function (s) { return String(s || '').trim(); })
352 .filter(Boolean);
353
354 var data = {};
355
356 fields.forEach(function (sel) {
357 var $f = $(sel);
358 if (!$f.length) { return; }
359
360 // If selector points to a wrapper, try to locate a real control inside.
361 var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first();
362 if (!$control.length) { return; }
363
364 var key = $control.attr('name') || $control.attr('id');
365 if (!key) { return; }
366
367 data[key] = wpbc_uix_get_control_value($control);
368 });
369
370 payload = $.param(data);
371 }
372 else {
373 console.error('WPBC | provide value, value-from selector, json, or fields');
374 return;
375 }
376
377 // Sync jQuery cache for the scalar value (helps other code that still reads .data()).
378 // If payload looks like a simple scalar (not JSON, not query-string), keep it aligned.
379 if (typeof payload === 'string' && payload.indexOf('=') === -1 && payload.indexOf('&') === -1) {
380 try {
381 $el.data('wpbc-u-save-value', payload);
382 } catch (e) {}
383 }
384
385 $(document).trigger('wpbc:option:beforeSave', [ $el, payload ]);
386 wpbc_uix_busy_on($el);
387
388 $.ajax({
389 url: w.wpbc_option_saver_loader_config.ajax_url,
390 type: 'POST',
391 data: {
392 action: w.wpbc_option_saver_loader_config.action_save,
393 nonce: nonce,
394 nonce_action: nonce_action,
395 data_name: data_name,
396 data_value: payload,
397
398 // Optional: split JSON object into separate options server-side.
399 data_mode: save_mode,
400
401 // Optional allowlist of keys for split mode (CSV).
402 data_fields: (save_mode === 'split' ? String(fields_raw || '') : '')
403 }
404 })
405 .done(function (resp) {
406
407 // NOTE: previously the code always showed "success" even on error.
408 // Fixed: show success only when resp.success is true.
409
410 if (resp && resp.success) {
411
412 $el.attr('data-wpbc-u-autosave-dirty', '0');
413 if (data_name && wpbc_uix_autosave_registry[data_name]) {
414 wpbc_uix_autosave_registry[data_name].dirty = false;
415 }
416
417 if (cb_fn) {
418 try { cb_fn(resp); } catch (e) { console.error(e); }
419 }
420
421 var ok_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Saved';
422 if (typeof w.wpbc_admin_show_message === 'function') {
423 w.wpbc_admin_show_message(ok_message, 'success', 1000, false);
424 }
425
426 } else {
427
428 var err_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Save error';
429 console.error('WPBC | ' + err_message);
430
431 if (typeof w.wpbc_admin_show_message === 'function') {
432 w.wpbc_admin_show_message(err_message, 'error', 30000);
433 }
434 }
435
436 $(document).trigger('wpbc:option:afterSave', [ resp ]);
437 })
438 .fail(function (xhr) {
439 var feedback_message = 'WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText;
440 console.error(feedback_message);
441
442 if (typeof w.wpbc_admin_show_message === 'function') {
443 w.wpbc_admin_show_message(feedback_message, 'error', 30000);
444 }
445
446 $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]);
447 })
448 .always(function () {
449 wpbc_uix_busy_off($el);
450 });
451 };
452
453 /**
454 * Wire opt-in autosave of global options after successful BFB form save.
455 *
456 * Add data-wpbc-u-autosave-on-form-save="1" to a save control to participate.
457 * Dirty state is tracked from data-wpbc-u-save-value-from, or data-wpbc-u-autosave-watch when present.
458 *
459 * @returns {void}
460 */
461 function wpbc_uix_bind_autosave_on_form_save() {
462 var autosave_selector = '[data-wpbc-u-autosave-on-form-save="1"]';
463
464 $(document).on('change input', 'input,select,textarea', function () {
465 var changed_el = this;
466
467 $(autosave_selector).each(function () {
468 var $btn = $(this);
469 var watch_selector = $btn.attr('data-wpbc-u-autosave-watch') || $btn.attr('data-wpbc-u-save-value-from') || '';
470
471 if (!watch_selector) {
472 return;
473 }
474
475 var $watched = $(watch_selector);
476 if ($watched.filter(changed_el).length || $watched.has(changed_el).length) {
477 $btn.attr('data-wpbc-u-autosave-dirty', '1');
478 wpbc_uix_update_autosave_registry_from_element(this, true);
479 }
480 });
481 });
482
483 document.addEventListener('wpbc:bfb:form:ajax_saved', function () {
484 $(autosave_selector).each(function () {
485 if (this.getAttribute('data-wpbc-u-autosave-dirty') === '1') {
486 wpbc_uix_update_autosave_registry_from_element(this, true);
487 }
488 });
489
490 Object.keys(wpbc_uix_autosave_registry).forEach(function (option_name) {
491 var entry = wpbc_uix_autosave_registry[option_name];
492 if (!entry || !entry.dirty) {
493 return;
494 }
495
496 if (entry.el && document.documentElement.contains(entry.el)) {
497 w.wpbc_save_option_from_element(entry.el);
498 } else {
499 wpbc_uix_save_autosave_registry_entry(entry);
500 }
501 });
502 });
503 }
504
505 /**
506 * Load option value via AJAX.
507 *
508 * @param {HTMLElement} el element with data attributes.
509 * @returns {void}
510 */
511 w.wpbc_load_option_from_element = function (el) {
512
513 if (!w.wpbc_option_saver_loader_config) {
514 console.error('WPBC | config missing');
515 return;
516 }
517
518 var $el = $(el);
519 var name = $el.data('wpbc-u-load-name') || $el.data('wpbc-u-save-name');
520
521 var cb_id = $el.data('wpbc-u-load-callback');
522 var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null;
523
524 if (!name) {
525 console.error('WPBC | missing data-wpbc-u-load-name');
526 return;
527 }
528
529 $(document).trigger('wpbc:option:beforeLoad', [ $el, name ]);
530 wpbc_uix_busy_on($el);
531
532 $.ajax({
533 url: w.wpbc_option_saver_loader_config.ajax_url,
534 type: 'GET',
535 data: {
536 action: w.wpbc_option_saver_loader_config.action_load,
537 data_name: name
538 }
539 })
540 .done(function (resp) {
541 if (resp && resp.success) {
542 if (cb_fn) {
543 try { cb_fn(resp.data && resp.data.value); } catch (e) { console.error(e); }
544 }
545 } else {
546 console.error('WPBC | ' + (resp && resp.data && resp.data.message ? resp.data.message : 'Load error'));
547 }
548 $(document).trigger('wpbc:option:afterLoad', [ resp ]);
549 })
550 .fail(function (xhr) {
551 console.error('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText);
552 $(document).trigger('wpbc:option:afterLoad', [ { success: false, data: { message: xhr.statusText } } ]);
553 })
554 .always(function () {
555 wpbc_uix_busy_off($el);
556 });
557 };
558
559 wpbc_uix_bind_autosave_on_form_save();
560
561 }(window, jQuery));
562