/** * General Option Loader/Saver (client) * * - Provides: * window.wpbc_save_option_from_element(el) * window.wpbc_load_option_from_element(el) * - Busy UI (spinner + disabled) * - JSON path: send raw JSON string untouched. * - RAW scalar path: send as-is. * - Fields path: serialize to query-string via jQuery.param. * * IMPORTANT: * - jQuery .data() is cached. If some code updates data-* attributes via setAttribute(), * reading via $el.data(...) can return stale values. * - Therefore, this module prefers reading via $el.attr('data-...') for dynamic keys * (value/value-json), and falls back to $el.data(...) when attribute is missing. * * file: ../includes/save-load-option/_out/save-load-option.js * * Events: * $(document).on('wpbc:option:beforeSave', (e, $el, payload) => {}) * $(document).on('wpbc:option:afterSave', (e, response) => {}) * $(document).on('wpbc:option:beforeLoad', (e, $el, name) => {}) * $(document).on('wpbc:option:afterLoad', (e, response) => {}) */ (function (w, $) { 'use strict'; /** * Escape for safe HTML injection (small helper). * * @param {string} s * @returns {string} */ function wpbc_uix_escape_html(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Read a value from data-* attribute first (fresh), then fallback to jQuery .data() cache. * * @param {jQuery} $el * @param {string} attr_name Example: 'data-wpbc-u-save-value' * @param {string} data_key Example: 'wpbc-u-save-value' * @returns {*} */ function wpbc_uix_read_attr_or_data($el, attr_name, data_key) { var v = $el.attr(attr_name); if (typeof v !== 'undefined') { return v; } return $el.data(data_key); } /** * Turn "On"/"Off" like values into consistent "On"/"Off". * (Used for checkbox/toggle serialization.) * * @param {*} v * @returns {string} */ function wpbc_uix_to_on_off(v) { if (v === true) { return 'On'; } if (v === false) { return 'Off'; } var s = String(v || '').toLowerCase(); if (s === 'on' || s === '1' || s === 'true' || s === 'yes') { return 'On'; } return 'Off'; } /** * Get a useful value from an input/select/textarea element. * - checkbox => 'On'/'Off' * - radio => value of checked in group (if possible), else '' * - others => .val() * * @param {jQuery} $control * @returns {string} */ function wpbc_uix_get_control_value($control) { if (!$control || !$control.length) { return ''; } // checkbox/toggle. if ($control.is(':checkbox')) { return $control.is(':checked') ? 'On' : 'Off'; } // radio group. if ($control.is(':radio')) { var name = $control.attr('name'); if (name) { var $checked = $('input[type="radio"][name="' + name + '"]:checked'); return $checked.length ? String($checked.val()) : ''; } return $control.is(':checked') ? String($control.val()) : ''; } // select/text/textarea/etc. return String($control.val() == null ? '' : $control.val()); } /** * Busy ON UI for a clickable element. * * @param {jQuery} $el * @returns {void} */ function wpbc_uix_busy_on($el) { if (!$el || !$el.length || $el.data('wpbc-uix-busy')) { return; } $el.data('wpbc-uix-busy', 1); $el.data('wpbc-uix-original-html', $el.html()); var busy_text = $el.data('wpbc-u-busy-text'); var spinner = ''; if (typeof busy_text === 'string' && busy_text.length) { $el.html(wpbc_uix_escape_html(busy_text) + ' ' + spinner); } else { $el.append(spinner); } $el.addClass('wpbc-is-busy') .attr('aria-disabled', 'true') .prop('disabled', true); } /** * Busy OFF UI for a clickable element. * * @param {jQuery} $el * @returns {void} */ function wpbc_uix_busy_off($el) { if (!$el || !$el.length || !$el.data('wpbc-uix-busy')) { return; } var original = $el.data('wpbc-uix-original-html'); if (typeof original === 'string') { $el.html(original); } $el.removeClass('wpbc-is-busy') .removeAttr('aria-disabled') .prop('disabled', false); $el.removeData('wpbc-uix-busy') .removeData('wpbc-uix-original-html'); } var wpbc_uix_autosave_registry = {}; function wpbc_uix_get_option_save_config($el) { return { data_name : $el.data('wpbc-u-save-name'), fields_raw : $el.data('wpbc-u-save-fields') || '', inline_value : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value'), json : wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json'), value_from_selector: $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from') }; } function wpbc_uix_build_option_save_payload($el, cfg) { cfg = cfg || wpbc_uix_get_option_save_config($el); if (typeof cfg.json === 'string' && cfg.json.trim() !== '') { return cfg.json.trim(); } if (cfg.value_from_selector) { var $src = $(cfg.value_from_selector); var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first(); return wpbc_uix_get_control_value($control); } if (typeof cfg.inline_value !== 'undefined' && cfg.inline_value !== null) { return String(cfg.inline_value); } if (cfg.fields_raw) { var fields = String(cfg.fields_raw).split(',') .map(function (s) { return String(s || '').trim(); }) .filter(Boolean); var data = {}; fields.forEach(function (sel) { var $f = $(sel); if (!$f.length) { return; } var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first(); if (!$control.length) { return; } var key = $control.attr('name') || $control.attr('id'); if (!key) { return; } data[key] = wpbc_uix_get_control_value($control); }); return $.param(data); } return null; } function wpbc_uix_update_autosave_registry_from_element(el, is_dirty) { var $el = $(el); var cfg = wpbc_uix_get_option_save_config($el); if (!cfg.data_name) { return; } wpbc_uix_autosave_registry[cfg.data_name] = { data_name : cfg.data_name, payload : wpbc_uix_build_option_save_payload($el, cfg), dirty : !!is_dirty, el : el }; } function wpbc_uix_save_autosave_registry_entry(entry) { if (!entry || !entry.dirty || !entry.data_name || entry.payload === null || !w.wpbc_option_saver_loader_config || !w.wpbc_option_saver_loader_config.save_nonce) { return; } $(document).trigger('wpbc:option:beforeSave', [ $(), entry.payload ]); $.ajax({ url: w.wpbc_option_saver_loader_config.ajax_url, type: 'POST', data: { action: w.wpbc_option_saver_loader_config.action_save, nonce: w.wpbc_option_saver_loader_config.save_nonce, data_name: entry.data_name, data_value: entry.payload } }) .done(function (resp) { if (resp && resp.success) { entry.dirty = false; if (entry.el) { $(entry.el).attr('data-wpbc-u-autosave-dirty', '0'); } if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message((resp.data && resp.data.message) ? resp.data.message : 'Saved', 'success', 1000, false); } } else if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message((resp && resp.data && resp.data.message) ? resp.data.message : 'Save error', 'error', 30000); } $(document).trigger('wpbc:option:afterSave', [ resp ]); }) .fail(function (xhr) { if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText, 'error', 30000); } $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]); }); } /** * Save Option - send ajax request to save data. * * Data attributes: * data-wpbc-u-save-name — option key (required) * The fixed save nonce is supplied by wpbc_option_saver_loader_config. * data-wpbc-u-save-value — RAW scalar to save (optional) (dynamic: read via attr first) * data-wpbc-u-save-value-json — JSON string to save (optional) (dynamic: read via attr first) * data-wpbc-u-save-fields — CSV selectors serialized with jQuery.param (optional). Server policy owns the writable-key allowlist. * data-wpbc-u-save-value-from — OPTIONAL selector to read scalar from (checkbox => On/Off) * data-wpbc-u-busy-text — custom text during AJAX (optional) * data-wpbc-u-save-callback — window function name to call on success (optional) * * @param {HTMLElement} el element with data attributes. * @returns {void} */ w.wpbc_save_option_from_element = function (el) { if (!w.wpbc_option_saver_loader_config) { console.error('WPBC | config missing'); return; } var $el = $(el); var nonce = w.wpbc_option_saver_loader_config.save_nonce; var data_name = $el.data('wpbc-u-save-name'); // Dynamic values MUST prefer attribute read (fresh), fallback to .data(). var fields_raw = $el.data('wpbc-u-save-fields') || ''; var inline_value = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value', 'wpbc-u-save-value'); var json = wpbc_uix_read_attr_or_data($el, 'data-wpbc-u-save-value-json', 'wpbc-u-save-value-json'); // Optional: compute scalar from another control selector at click time. var value_from_selector = $el.data('wpbc-u-save-value-from') || $el.attr('data-wpbc-u-save-value-from'); var cb_id = $el.data('wpbc-u-save-callback'); var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null; if (!nonce || !data_name) { console.error('WPBC | missing nonce/name'); return; } var payload = ''; // 1) JSON path. if (typeof json === 'string' && json.trim() !== '') { payload = json.trim(); } // 2) Scalar computed from selector (checkbox => On/Off). else if (value_from_selector) { var $src = $(value_from_selector); var $control = $src.is('input,select,textarea') ? $src : $src.find('input,select,textarea').first(); payload = wpbc_uix_get_control_value($control); } // 3) RAW scalar path. else if (typeof inline_value !== 'undefined' && inline_value !== null) { payload = String(inline_value); } // 4) Fields path (query-string). else if (fields_raw) { var fields = String(fields_raw).split(',') .map(function (s) { return String(s || '').trim(); }) .filter(Boolean); var data = {}; fields.forEach(function (sel) { var $f = $(sel); if (!$f.length) { return; } // If selector points to a wrapper, try to locate a real control inside. var $control = $f.is('input,select,textarea') ? $f : $f.find('input,select,textarea').first(); if (!$control.length) { return; } var key = $control.attr('name') || $control.attr('id'); if (!key) { return; } data[key] = wpbc_uix_get_control_value($control); }); payload = $.param(data); } else { console.error('WPBC | provide value, value-from selector, json, or fields'); return; } // Sync jQuery cache for the scalar value (helps other code that still reads .data()). // If payload looks like a simple scalar (not JSON, not query-string), keep it aligned. if (typeof payload === 'string' && payload.indexOf('=') === -1 && payload.indexOf('&') === -1) { try { $el.data('wpbc-u-save-value', payload); } catch (e) {} } $(document).trigger('wpbc:option:beforeSave', [ $el, payload ]); wpbc_uix_busy_on($el); $.ajax({ url: w.wpbc_option_saver_loader_config.ajax_url, type: 'POST', data: { action: w.wpbc_option_saver_loader_config.action_save, nonce: nonce, data_name: data_name, data_value: payload } }) .done(function (resp) { // NOTE: previously the code always showed "success" even on error. // Fixed: show success only when resp.success is true. if (resp && resp.success) { $el.attr('data-wpbc-u-autosave-dirty', '0'); if (data_name && wpbc_uix_autosave_registry[data_name]) { wpbc_uix_autosave_registry[data_name].dirty = false; } if (cb_fn) { try { cb_fn(resp); } catch (e) { console.error(e); } } var ok_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Saved'; if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message(ok_message, 'success', 1000, false); } } else { var err_message = (resp && resp.data && resp.data.message) ? resp.data.message : 'Save error'; console.error('WPBC | ' + err_message); if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message(err_message, 'error', 30000); } } $(document).trigger('wpbc:option:afterSave', [ resp ]); }) .fail(function (xhr) { var feedback_message = 'WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText; console.error(feedback_message); if (typeof w.wpbc_admin_show_message === 'function') { w.wpbc_admin_show_message(feedback_message, 'error', 30000); } $(document).trigger('wpbc:option:afterSave', [ { success: false, data: { message: xhr.statusText } } ]); }) .always(function () { wpbc_uix_busy_off($el); }); }; /** * Wire opt-in autosave of global options after successful BFB form save. * * Add data-wpbc-u-autosave-on-form-save="1" to a save control to participate. * Dirty state is tracked from data-wpbc-u-save-value-from, or data-wpbc-u-autosave-watch when present. * * @returns {void} */ function wpbc_uix_bind_autosave_on_form_save() { var autosave_selector = '[data-wpbc-u-autosave-on-form-save="1"]'; $(document).on('change input', 'input,select,textarea', function () { var changed_el = this; $(autosave_selector).each(function () { var $btn = $(this); var watch_selector = $btn.attr('data-wpbc-u-autosave-watch') || $btn.attr('data-wpbc-u-save-value-from') || ''; if (!watch_selector) { return; } var $watched = $(watch_selector); if ($watched.filter(changed_el).length || $watched.has(changed_el).length) { $btn.attr('data-wpbc-u-autosave-dirty', '1'); wpbc_uix_update_autosave_registry_from_element(this, true); } }); }); document.addEventListener('wpbc:bfb:form:ajax_saved', function () { $(autosave_selector).each(function () { if (this.getAttribute('data-wpbc-u-autosave-dirty') === '1') { wpbc_uix_update_autosave_registry_from_element(this, true); } }); Object.keys(wpbc_uix_autosave_registry).forEach(function (option_name) { var entry = wpbc_uix_autosave_registry[option_name]; if (!entry || !entry.dirty) { return; } if (entry.el && document.documentElement.contains(entry.el)) { w.wpbc_save_option_from_element(entry.el); } else { wpbc_uix_save_autosave_registry_entry(entry); } }); }); } /** * Load option value via AJAX. * * @param {HTMLElement} el element with data attributes. * @returns {void} */ w.wpbc_load_option_from_element = function (el) { if (!w.wpbc_option_saver_loader_config) { console.error('WPBC | config missing'); return; } var $el = $(el); var name = $el.data('wpbc-u-load-name') || $el.data('wpbc-u-save-name'); var cb_id = $el.data('wpbc-u-load-callback'); var cb_fn = (cb_id && typeof w[cb_id] === 'function') ? w[cb_id] : null; if (!name || !w.wpbc_option_saver_loader_config.load_nonce) { console.error('WPBC | missing load nonce/name'); return; } $(document).trigger('wpbc:option:beforeLoad', [ $el, name ]); wpbc_uix_busy_on($el); $.ajax({ url: w.wpbc_option_saver_loader_config.ajax_url, type: 'GET', data: { action: w.wpbc_option_saver_loader_config.action_load, nonce: w.wpbc_option_saver_loader_config.load_nonce, data_name: name } }) .done(function (resp) { if (resp && resp.success) { if (cb_fn) { try { cb_fn(resp.data && resp.data.value); } catch (e) { console.error(e); } } } else { console.error('WPBC | ' + (resp && resp.data && resp.data.message ? resp.data.message : 'Load error')); } $(document).trigger('wpbc:option:afterLoad', [ resp ]); }) .fail(function (xhr) { console.error('WPBC | AJAX ' + xhr.status + ' ' + xhr.statusText); $(document).trigger('wpbc:option:afterLoad', [ { success: false, data: { message: xhr.statusText } } ]); }) .always(function () { wpbc_uix_busy_off($el); }); }; wpbc_uix_bind_autosave_on_form_save(); }(window, jQuery));