diff --git a/admin/assets/wpdo-admin.css b/admin/assets/wpdo-admin.css index 78722e0..0c284cc 100644 --- a/admin/assets/wpdo-admin.css +++ b/admin/assets/wpdo-admin.css @@ -513,3 +513,31 @@ animation: none; } } + +/* ── Settings per-section instant save (v3.0.2 backport) ────────────── */ + +.wpdo-settings-section { + background: var(--color-surface-card, #fbf7f0); + border: 1px solid var(--color-border-soft, #e5dccd); + border-radius: var(--radius-md, 12px); + padding: var(--space-4, 16px) var(--space-6, 24px); + margin-bottom: var(--space-5, 20px); +} + +.wpdo-section-save-row { + display: flex; + align-items: center; + gap: var(--space-3, 12px); + padding-top: var(--space-3, 12px); +} + +.wpdo-section-save-status { + font-size: 0.9rem; + color: var(--color-success, #5a8c5a); + opacity: 0; + transition: opacity var(--duration-normal, 200ms) var(--ease-default, ease); +} + +.wpdo-section-save-status.visible { + opacity: 1; +} diff --git a/admin/assets/wpdo-settings.js b/admin/assets/wpdo-settings.js new file mode 100644 index 0000000..8cf7cc2 --- /dev/null +++ b/admin/assets/wpdo-settings.js @@ -0,0 +1,106 @@ +/** + * WP Data Optimizer — Settings per-section instant save. + * + * Each .wpdo-settings-section card has a "儲存此區塊" button that posts only + * that section's fields via AJAX, updating options without a full page reload. + * + * @since 3.0.2 + */ +/* global wpdoSettings */ +( function () { + 'use strict'; + + if ( typeof wpdoSettings === 'undefined' ) { + return; + } + + const { ajaxUrl, nonce, i18n } = wpdoSettings; + + /** + * Collect form fields within a section into a FormData object. + * Unchecked checkboxes are intentionally omitted (server does isset() ? '1' : '0'). + * + * @param {HTMLElement} section + * @returns {FormData} + */ + function collectFields( section ) { + const fd = new FormData(); + section.querySelectorAll( 'input, select, textarea' ).forEach( ( el ) => { + if ( ! el.name ) { + return; + } + if ( el.type === 'checkbox' ) { + if ( el.checked ) { + fd.append( el.name, el.value ); + } + } else if ( el.type === 'radio' ) { + if ( el.checked ) { + fd.append( el.name, el.value ); + } + } else { + fd.append( el.name, el.value ); + } + } ); + return fd; + } + + /** + * Show a transient status message, then fade it out after 2.5s. + * + * @param {HTMLElement} statusEl + * @param {string} message + * @param {boolean} isError + */ + function showStatus( statusEl, message, isError ) { + statusEl.textContent = message; + statusEl.style.color = isError ? 'var(--color-error, #b03d3d)' : 'var(--color-success, #5a8c5a)'; + statusEl.classList.add( 'visible' ); + clearTimeout( statusEl._wpdo_timer ); + statusEl._wpdo_timer = setTimeout( () => { + statusEl.classList.remove( 'visible' ); + }, 2500 ); + } + + document.addEventListener( 'click', function ( e ) { + const btn = e.target.closest( '.wpdo-section-save' ); + if ( ! btn ) { + return; + } + const section = btn.closest( '.wpdo-settings-section' ); + const statusEl = btn.nextElementSibling; + const sectionKey = section ? section.dataset.section : ''; + if ( ! sectionKey ) { + return; + } + + const originalLabel = btn.textContent; + btn.disabled = true; + btn.textContent = i18n.saving; + + const fd = collectFields( section ); + fd.append( 'action', 'wpdo_save_settings_section' ); + fd.append( '_ajax_nonce', nonce ); + fd.append( 'section', sectionKey ); + + fetch( ajaxUrl, { + method : 'POST', + credentials : 'same-origin', + body : fd, + } ) + .then( ( r ) => r.json() ) + .then( ( data ) => { + if ( data.success ) { + showStatus( statusEl, i18n.saved, false ); + } else { + showStatus( statusEl, i18n.error, true ); + } + } ) + .catch( () => { + showStatus( statusEl, i18n.error, true ); + } ) + .finally( () => { + btn.disabled = false; + btn.textContent = originalLabel; + } ); + } ); +} )(); diff --git a/admin/class-tmdo-admin.php b/admin/class-tmdo-admin.php index 13eaf4a..97846eb 100644 --- a/admin/class-tmdo-admin.php +++ b/admin/class-tmdo-admin.php @@ -44,6 +44,7 @@ class TMDO_Admin { add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'network_admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'wp_ajax_wpdo_admin_action', array( __CLASS__, 'handle_ajax' ) ); + add_action( 'wp_ajax_wpdo_save_settings_section', array( __CLASS__, 'ajax_save_settings_section' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_nginx_backup_notice' ) ); } @@ -300,6 +301,28 @@ class TMDO_Admin { ), ) ); + + // Settings per-section instant save. + wp_enqueue_script( + 'wpdo-settings', + TMDO_URL . 'admin/assets/wpdo-settings.js', + array(), + TMDO_VERSION, + true + ); + wp_localize_script( + 'wpdo-settings', + 'wpdoSettings', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wpdo_save_settings' ), + 'i18n' => array( + 'saving' => __( '儲存中…', '2meet-data-optimizer' ), + 'saved' => __( '✓ 已儲存', '2meet-data-optimizer' ), + 'error' => __( '儲存失敗,請重試。', '2meet-data-optimizer' ), + ), + ) + ); } /** @@ -3211,6 +3234,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' +