PluginProbe
Booking Calendar / 11.8.3
Booking Calendar v11.8.3
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.8.3, at includes/save-load-option/_src/save-load-option.js

541 lines 16.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 data_name : $el.data('wpbc-u-save-name'),
166 fields_raw : $el.data('wpbc-u-save-fields') || '',
167 inline_value : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value'),
168 json : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json'),
169 value_from_selector: $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from')
170 };
171 }
172
173 function wpbc_uix_build_option_save_payload($el, cfg) {
174 cfg = cfg || wpbc_uix_get_option_save_config($el);
175
176 if (typeof cfg.json === 'string' && cfg.json.trim() !== '') {
177 return cfg.json.trim();
178 }
179
180 if (cfg.value_from_selector) {
181 var $src = $(cfg.value_from_selector);
182 var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first();
183 return wpbc_uix_get_control_value($control);
184 }
185
186 if (typeof cfg.inline_value !== 'undefined' && cfg.inline_value !== null) {
187 return String(cfg.inline_value);
188 }
189
190 if (cfg.fields_raw) {
191 var fields = String(cfg.fields_raw).split(',')
192 .map(function (s) { return String(s || '').trim(); })
193 .filter(Boolean);
194
195 var data = {};
196
197 fields.forEach(function (sel) {
198 var $f = $(sel);
199 if (!$f.length) { return; }
200
201 var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first();
202 if (!$control.length) { return; }
203
204 var key = $control.attr('name') || $control.attr('id');
205 if (!key) { return; }
206
207 data[key] = wpbc_uix_get_control_value($control);
208 });
209
210 return $.param(data);
211 }
212
213 return null;
214 }
215
216 function wpbc_uix_update_autosave_registry_from_element(el, is_dirty) {
217 var $el = $(el);
218 var cfg = wpbc_uix_get_option_save_config($el);
219
220 if (!cfg.data_name) {
221 return;
222 }
223
224 wpbc_uix_autosave_registry[cfg.data_name] = {
225 data_name : cfg.data_name,
226 payload : wpbc_uix_build_option_save_payload($el, cfg),
227 dirty : !!is_dirty,
228 el : el
229 };
230 }
231
232 function wpbc_uix_save_autosave_registry_entry(entry) {
233 if (!entry || !entry.dirty || !entry.data_name || entry.payload === null || !w.wpbc_option_saver_loader_config || !w.wpbc_option_saver_loader_config.save_nonce) {
234 return;
235 }
236
237 $(document).trigger('wpbc:option:beforeSave', [ $(), entry.payload ]);
238
239 $.ajax({
240 url: w.wpbc_option_saver_loader_config.ajax_url,
241 type: 'POST',
242 data: {
243 action: w.wpbc_option_saver_loader_config.action_save,
244 nonce: w.wpbc_option_saver_loader_config.save_nonce,
245 data_name: entry.data_name,
246 data_value: entry.payload
247 }
248 })
249 .done(function (resp) {
250 if (resp && resp.success) {
251 entry.dirty = false;
252 if (entry.el) {
253 $(entry.el).attr('data-wpbc-u-autosave-dirty', '0');
254 }
255 if (typeof w.wpbc_admin_show_message === 'function') {
256 w.wpbc_admin_show_message((resp.data && resp.data.message) ? resp.data.message : 'Saved', 'success', 1000, false);
257 }
258 } else if (typeof w.wpbc_admin_show_message === 'function') {
259 w.wpbc_admin_show_message((resp && resp.data && resp.data.message) ? resp.data.message : 'Save error', 'error', 30000);
260 }
261
262 $(document).trigger('wpbc:option:afterSave', [ resp ]);
263 })
264 .fail(function (xhr) {
265 if (typeof w.wpbc_admin_show_message === 'function') {
266 w.wpbc_admin_show_message('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText, 'error', 30000);
267 }
268 $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]);
269 });
270 }
271
272 /**
273 * Save Option - send ajax request to save data.
274 *
275 * Data attributes:
276 * data-wpbc-u-save-name — option key (required)
277 * The fixed save nonce is supplied by wpbc_option_saver_loader_config.
278 * data-wpbc-u-save-value — RAW scalar to save (optional) (dynamic: read via attr first)
279 * data-wpbc-u-save-value-json — JSON string to save (optional) (dynamic: read via attr first)
280 * data-wpbc-u-save-fields — CSV selectors serialized with jQuery.param (optional). Server policy owns the writable-key allowlist.
281 * data-wpbc-u-save-value-from — OPTIONAL selector to read scalar from (checkbox => On/Off)
282 * data-wpbc-u-busy-text — custom text during AJAX (optional)
283 * data-wpbc-u-save-callback — window function name to call on success (optional)
284 *
285 * @param {HTMLElement} el element with data attributes.
286 * @returns {void}
287 */
288 w.wpbc_save_option_from_element = function (el) {
289
290 if (!w.wpbc_option_saver_loader_config) {
291 console.error('WPBC | config missing');
292 return;
293 }
294
295 var $el = $(el);
296
297 var nonce = w.wpbc_option_saver_loader_config.save_nonce;
298 var data_name = $el.data('wpbc-u-save-name');
299
300 // Dynamic values MUST prefer attribute read (fresh), fallback to .data().
301 var fields_raw = $el.data('wpbc-u-save-fields') || '';
302 var inline_value = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value');
303 var json = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json');
304
305 // Optional: compute scalar from another control selector at click time.
306 var value_from_selector = $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from');
307
308 var cb_id = $el.data('wpbc-u-save-callback');
309 var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null;
310
311 if (!nonce || !data_name) {
312 console.error('WPBC | missing nonce/name');
313 return;
314 }
315
316 var payload = '';
317
318 // 1) JSON path.
319 if (typeof json === 'string' && json.trim() !== '') {
320 payload = json.trim();
321 }
322 // 2) Scalar computed from selector (checkbox => On/Off).
323 else if (value_from_selector) {
324 var $src = $(value_from_selector);
325 var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first();
326 payload = wpbc_uix_get_control_value($control);
327 }
328 // 3) RAW scalar path.
329 else if (typeof inline_value !== 'undefined' && inline_value !== null) {
330 payload = String(inline_value);
331 }
332 // 4) Fields path (query-string).
333 else if (fields_raw) {
334
335 var fields = String(fields_raw).split(',')
336 .map(function (s) { return String(s || '').trim(); })
337 .filter(Boolean);
338
339 var data = {};
340
341 fields.forEach(function (sel) {
342 var $f = $(sel);
343 if (!$f.length) { return; }
344
345 // If selector points to a wrapper, try to locate a real control inside.
346 var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first();
347 if (!$control.length) { return; }
348
349 var key = $control.attr('name') || $control.attr('id');
350 if (!key) { return; }
351
352 data[key] = wpbc_uix_get_control_value($control);
353 });
354
355 payload = $.param(data);
356 }
357 else {
358 console.error('WPBC | provide value, value-from selector, json, or fields');
359 return;
360 }
361
362 // Sync jQuery cache for the scalar value (helps other code that still reads .data()).
363 // If payload looks like a simple scalar (not JSON, not query-string), keep it aligned.
364 if (typeof payload === 'string' && payload.indexOf('=') === -1 && payload.indexOf('&') === -1) {
365 try {
366 $el.data('wpbc-u-save-value', payload);
367 } catch (e) {}
368 }
369
370 $(document).trigger('wpbc:option:beforeSave', [ $el, payload ]);
371 wpbc_uix_busy_on($el);
372
373 $.ajax({
374 url: w.wpbc_option_saver_loader_config.ajax_url,
375 type: 'POST',
376 data: {
377 action: w.wpbc_option_saver_loader_config.action_save,
378 nonce: nonce,
379 data_name: data_name,
380 data_value: payload
381 }
382 })
383 .done(function (resp) {
384
385 // NOTE: previously the code always showed "success" even on error.
386 // Fixed: show success only when resp.success is true.
387
388 if (resp && resp.success) {
389
390 $el.attr('data-wpbc-u-autosave-dirty', '0');
391 if (data_name && wpbc_uix_autosave_registry[data_name]) {
392 wpbc_uix_autosave_registry[data_name].dirty = false;
393 }
394
395 if (cb_fn) {
396 try { cb_fn(resp); } catch (e) { console.error(e); }
397 }
398
399 var ok_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Saved';
400 if (typeof w.wpbc_admin_show_message === 'function') {
401 w.wpbc_admin_show_message(ok_message, 'success', 1000, false);
402 }
403
404 } else {
405
406 var err_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Save error';
407 console.error('WPBC | ' + err_message);
408
409 if (typeof w.wpbc_admin_show_message === 'function') {
410 w.wpbc_admin_show_message(err_message, 'error', 30000);
411 }
412 }
413
414 $(document).trigger('wpbc:option:afterSave', [ resp ]);
415 })
416 .fail(function (xhr) {
417 var feedback_message = 'WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText;
418 console.error(feedback_message);
419
420 if (typeof w.wpbc_admin_show_message === 'function') {
421 w.wpbc_admin_show_message(feedback_message, 'error', 30000);
422 }
423
424 $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]);
425 })
426 .always(function () {
427 wpbc_uix_busy_off($el);
428 });
429 };
430
431 /**
432 * Wire opt-in autosave of global options after successful BFB form save.
433 *
434 * Add data-wpbc-u-autosave-on-form-save="1" to a save control to participate.
435 * Dirty state is tracked from data-wpbc-u-save-value-from, or data-wpbc-u-autosave-watch when present.
436 *
437 * @returns {void}
438 */
439 function wpbc_uix_bind_autosave_on_form_save() {
440 var autosave_selector = '[data-wpbc-u-autosave-on-form-save="1"]';
441
442 $(document).on('change input', 'input,select,textarea', function () {
443 var changed_el = this;
444
445 $(autosave_selector).each(function () {
446 var $btn = $(this);
447 var watch_selector = $btn.attr('data-wpbc-u-autosave-watch') || $btn.attr('data-wpbc-u-save-value-from') || '';
448
449 if (!watch_selector) {
450 return;
451 }
452
453 var $watched = $(watch_selector);
454 if ($watched.filter(changed_el).length || $watched.has(changed_el).length) {
455 $btn.attr('data-wpbc-u-autosave-dirty', '1');
456 wpbc_uix_update_autosave_registry_from_element(this, true);
457 }
458 });
459 });
460
461 document.addEventListener('wpbc:bfb:form:ajax_saved', function () {
462 $(autosave_selector).each(function () {
463 if (this.getAttribute('data-wpbc-u-autosave-dirty') === '1') {
464 wpbc_uix_update_autosave_registry_from_element(this, true);
465 }
466 });
467
468 Object.keys(wpbc_uix_autosave_registry).forEach(function (option_name) {
469 var entry = wpbc_uix_autosave_registry[option_name];
470 if (!entry || !entry.dirty) {
471 return;
472 }
473
474 if (entry.el && document.documentElement.contains(entry.el)) {
475 w.wpbc_save_option_from_element(entry.el);
476 } else {
477 wpbc_uix_save_autosave_registry_entry(entry);
478 }
479 });
480 });
481 }
482
483 /**
484 * Load option value via AJAX.
485 *
486 * @param {HTMLElement} el element with data attributes.
487 * @returns {void}
488 */
489 w.wpbc_load_option_from_element = function (el) {
490
491 if (!w.wpbc_option_saver_loader_config) {
492 console.error('WPBC | config missing');
493 return;
494 }
495
496 var $el = $(el);
497 var name = $el.data('wpbc-u-load-name') || $el.data('wpbc-u-save-name');
498
499 var cb_id = $el.data('wpbc-u-load-callback');
500 var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null;
501
502 if (!name || !w.wpbc_option_saver_loader_config.load_nonce) {
503 console.error('WPBC | missing load nonce/name');
504 return;
505 }
506
507 $(document).trigger('wpbc:option:beforeLoad', [ $el, name ]);
508 wpbc_uix_busy_on($el);
509
510 $.ajax({
511 url: w.wpbc_option_saver_loader_config.ajax_url,
512 type: 'GET',
513 data: {
514 action: w.wpbc_option_saver_loader_config.action_load,
515 nonce: w.wpbc_option_saver_loader_config.load_nonce,
516 data_name: name
517 }
518 })
519 .done(function (resp) {
520 if (resp && resp.success) {
521 if (cb_fn) {
522 try { cb_fn(resp.data && resp.data.value); } catch (e) { console.error(e); }
523 }
524 } else {
525 console.error('WPBC | ' + (resp && resp.data && resp.data.message ? resp.data.message : 'Load error'));
526 }
527 $(document).trigger('wpbc:option:afterLoad', [ resp ]);
528 })
529 .fail(function (xhr) {
530 console.error('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText);
531 $(document).trigger('wpbc:option:afterLoad', [ { success: false, data: { message: xhr.statusText } } ]);
532 })
533 .always(function () {
534 wpbc_uix_busy_off($el);
535 });
536 };
537
538 wpbc_uix_bind_autosave_on_form_save();
539
540 }(window, jQuery));
541