✏️ 正在编辑: woocommerce.js
路径:
/home/h359620/public_html/wp-content/themes/datis/assets/js/woocommerce.js
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
/** * ----------------------------------------------------------------------------- * Datis WooCommerce Front Scripts * ----------------------------------------------------------------------------- * Includes: * - Cart AJAX quantity/update/remove (DATIS_CART) * - Single product UI (qty buttons, swipers) * - Archive price filter slider (DATIS_WC) * - Misc UI behaviors * * Notes: * - Uses WooCommerce formatting config passed via wp_localize_script * - Avoids HTML-based formatting in JS outputs (returns plain text) * ----------------------------------------------------------------------------- */ /* ============================================================================ * Shared Helpers * ========================================================================== */ /** * Format a number using WooCommerce-like settings (decimals, separators). * Returns a PLAIN STRING (no HTML). * * @param {number|string} amount * @param {object} cfg - {decimals, decimalSep, thousandSep, currencySymbol, priceFormat} * @param {boolean} withSymbol - whether to include the currency symbol in output * @returns {string} */ function datisFormatWooPrice(amount, cfg, withSymbol = true) { const n = Number(amount ?? 0); const decimals = Number(cfg?.decimals ?? 0); const dec = cfg?.decimalSep ?? '.'; const thou = cfg?.thousandSep ?? ','; const symbol = cfg?.currencySymbol ?? ''; const fmt = cfg?.priceFormat ?? '%1$s%2$s'; // Force fixed decimals (Woo-like) let s = Number.isFinite(n) ? n.toFixed(decimals) : (0).toFixed(decimals); // Apply thousand separators to integer part const parts = s.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thou); // Join with Woo decimal separator s = (parts.length > 1) ? (parts[0] + dec + parts[1]) : parts[0]; // Apply Woo price format placeholders // Woo usually uses: %1$s = currency symbol, %2$s = number const sym = withSymbol ? symbol : ''; return fmt.replace('%1$s', sym).replace('%2$s', s); } /* ============================================================================ * CART * ========================================================================== */ (function () { if (typeof DATIS_CART === 'undefined') return; /** * Build WooCommerce wc-ajax endpoint URL. * DATIS_CART.ajaxUrl typically looks like: * https://site.com/?wc-ajax=%%endpoint%% */ function ajaxEndpoint(name) { return DATIS_CART.ajaxUrl.replace('%%endpoint%%', name); } /** * POST helper (x-www-form-urlencoded) */ async function post(url, data) { const body = new URLSearchParams(); Object.keys(data || {}).forEach((k) => body.append(k, data[k])); const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body, credentials: 'same-origin', }); const txt = await res.text(); try { return JSON.parse(txt); } catch (e) { return { success: false, data: { message: 'Invalid JSON response', raw: txt } }; } } function getCartRoot() { return document.getElementById('datis-cart-root'); } /** * Format price using DATIS_CART config (Woo settings). * Returns HTML-safe string (plain text). */ function formatCartPrice(amount) { return datisFormatWooPrice(amount, DATIS_CART, true); } /** * Update per-line UI totals when quantity changes (optimistic UI update). */ function updateLinePricingUI(input) { const cartItem = input.closest('.cart-item'); if (!cartItem) return; const priceWrap = cartItem.querySelector('.price[data-unit-regular][data-unit-discounted]'); if (!priceWrap) return; const unitRegular = Number(priceWrap.getAttribute('data-unit-regular') || 0); const unitDisc = Number(priceWrap.getAttribute('data-unit-discounted') || 0); const qty = parseInt(String(input.value || '0').replace(/[^\d]/g, ''), 10) || 0; const totalRegular = unitRegular * qty; const totalDisc = unitDisc * qty; let percent = 0; if (totalRegular > 0 && totalDisc < totalRegular) { percent = Math.round(((totalRegular - totalDisc) / totalRegular) * 100); } const percentEl = priceWrap.querySelector('.discount-percentage'); const regularEl = priceWrap.querySelector('.regular-price'); const amountEl = priceWrap.querySelector('.price-amount'); if (percentEl) percentEl.textContent = percent > 0 ? `${percent}%` : ''; if (regularEl) regularEl.innerHTML = percent > 0 ? formatCartPrice(totalRegular) : ''; if (amountEl) amountEl.innerHTML = formatCartPrice(totalDisc); } // ------------------------- // Binding (events) // ------------------------- let busy = false; function withLock(fn) { return async function (...args) { if (busy) return; busy = true; try { await fn(...args); } finally { busy = false; } }; } function bindRemove() { document.querySelectorAll('.remove-product[data-cart_item_key]').forEach((el) => { if (el.dataset.bound) return; el.dataset.bound = '1'; el.addEventListener( 'click', withLock(async (e) => { e.preventDefault(); const key = el.getAttribute('data-cart_item_key'); if (!key) return; await removeItem(key); }) ); }); } function bindQty() { document.querySelectorAll('[data-qty-control]').forEach((wrap) => { if (wrap.dataset.bound) return; const input = wrap.querySelector('input[data-cart_item_key]'); const plus = wrap.querySelector('.qty-plus'); const minus = wrap.querySelector('.qty-minus'); if (!input || !plus || !minus) return; wrap.dataset.bound = '1'; const key = input.getAttribute('data-cart_item_key'); const getVal = () => parseInt(String(input.value || '0').replace(/[^\d]/g, ''), 10) || 0; const clamp = (v) => { let qty = Math.max(0, v); const maxAttr = input.getAttribute('max'); if (maxAttr) { const max = parseInt(maxAttr, 10); if (!isNaN(max)) qty = Math.min(qty, max); } return qty; }; let timer = null; const schedule = () => { clearTimeout(timer); timer = setTimeout( withLock(async () => { const qty = clamp(getVal()); input.value = qty; // Optimistic UI update updateLinePricingUI(input); await setQty(key, qty); }), 500 ); }; plus.addEventListener('click', () => { input.value = clamp(getVal() + 1); updateLinePricingUI(input); schedule(); }); minus.addEventListener('click', () => { input.value = clamp(getVal() - 1); updateLinePricingUI(input); schedule(); }); input.addEventListener('change', () => { input.value = clamp(getVal()); updateLinePricingUI(input); schedule(); }); input.addEventListener('blur', () => { input.value = clamp(getVal()); updateLinePricingUI(input); }); }); } function bindCouponToggle() { const btn = document.querySelector('.discount-code-btn'); const panel = document.getElementById('coupon-panel'); if (!btn || !panel) return; if (btn.dataset.bound) return; btn.dataset.bound = '1'; btn.addEventListener('click', function () { const isOpen = !panel.hasAttribute('hidden'); if (isOpen) { panel.setAttribute('hidden', ''); btn.setAttribute('aria-expanded', 'false'); } else { panel.removeAttribute('hidden'); btn.setAttribute('aria-expanded', 'true'); const input = panel.querySelector('input[name="coupon_code"]'); if (input) input.focus(); } }); } function bind() { bindRemove(); bindQty(); bindCouponToggle(); } /** * Refresh full cart HTML via custom endpoint. * Endpoint output should include <div id="datis-cart-root">...</div> */ async function refreshCartPage() { const r = await post(ajaxEndpoint('datis_get_cart_page'), { nonce: DATIS_CART.nonce }); if (!r || !r.success || !r.data || !r.data.html) return; const current = getCartRoot(); if (!current) return; const wrap = document.createElement('div'); wrap.innerHTML = r.data.html; const fresh = wrap.querySelector('#datis-cart-root'); if (fresh) { current.replaceWith(fresh); } else { current.innerHTML = r.data.html; } bind(); } async function setQty(cartItemKey, qty) { await post(ajaxEndpoint('datis_set_cart_qty'), { nonce: DATIS_CART.nonce, cart_item_key: cartItemKey, quantity: qty, }); await refreshCartPage(); } async function removeItem(cartItemKey) { await post(ajaxEndpoint('remove_from_cart'), { cart_item_key: cartItemKey }); await refreshCartPage(); } document.addEventListener('DOMContentLoaded', bind); })(); /* ============================================================================ * CHECKOUT / UI (Optimized) * ========================================================================== */ jQuery(function ($) { const $page = $('.page-checkout'); if (!$page.length) return; /* ---------------------------- * Coupon toggle * -------------------------- */ (function initCouponToggle() { const btn = document.querySelector('.discount-code-btn'); const panel = document.getElementById('coupon-panel'); if (!btn || !panel) return; if (btn.dataset.bound) return; btn.dataset.bound = '1'; btn.addEventListener('click', function () { const isOpen = !panel.hasAttribute('hidden'); if (isOpen) { panel.setAttribute('hidden', ''); btn.setAttribute('aria-expanded', 'false'); } else { panel.removeAttribute('hidden'); btn.setAttribute('aria-expanded', 'true'); const input = panel.querySelector('input[name="coupon_code"]'); if (input) input.focus(); } }); })(); /* ---------------------------- * Shipping checkbox (custom) * -------------------------- */ function syncShippingUI() { const real = document.getElementById('ship-to-different-address-checkbox'); const custom = document.querySelector('.shipping-checkbox'); const $wrap = $('.shipping-wrapper'); if (!real || !custom.length && !custom || !$wrap.length) return; const checked = !!real.checked; // sync custom class if (custom) custom.classList.toggle('is-check', checked); // sync wrapper visibility (use jQuery for consistency) $wrap.css('display', checked ? 'flex' : 'none'); } // click on custom checkbox $(document).on('click', '.shipping-checkbox', function (e) { e.preventDefault(); const real = document.getElementById('ship-to-different-address-checkbox'); if (!real) return; // toggle real checkbox properly real.checked = !real.checked; // Woo expects change event $(real).trigger('change'); syncShippingUI(); }); /* ---------------------------- * Payment method custom check * -------------------------- */ function syncPaymentUI() { // remove all checks $('.wc_payment_method').removeClass('is-check'); // find currently selected gateway input const $checked = $('input[name="payment_method"]:checked'); if (!$checked.length) return; // add check to its wrapper $checked.closest('.wc_payment_method').addClass('is-check'); } // when user clicks a method (delegated) $(document).on('change', 'input[name="payment_method"]', function () { syncPaymentUI(); }); // some themes click .wc_payment_method itself $(document).on('click', '.wc_payment_method', function () { const $radio = $(this).find('input[name="payment_method"]'); if ($radio.length) { $radio.prop('checked', true).trigger('change'); } }); /* ---------------------------- * WooCommerce AJAX re-renders checkout * -------------------------- */ $(document.body).on('updated_checkout', function () { syncPaymentUI(); syncShippingUI(); }); /* ---------------------------- * Initial sync on page load * -------------------------- */ syncPaymentUI(); syncShippingUI(); }); /* ============================================================================ * SINGLE (your original) * ========================================================================== */ document.addEventListener('DOMContentLoaded', function () { const thumbs = new Swiper('.gallery-thumbs', { spaceBetween: 10, slidesPerView: 6, freeMode: true, loop: true, watchSlidesProgress: true, breakpoints: { 0: { slidesPerView: 4 }, 576: { slidesPerView: 5 }, 992: { slidesPerView: 6 } } }); const main = new Swiper('.gallery-swiper', { spaceBetween: 12, loop: true, thumbs: { swiper: thumbs } }); document.querySelectorAll('.product-section-nav .section-nav-item').forEach(item => { item.addEventListener('click', () => { document.querySelectorAll('.product-section-nav .section-nav-item') .forEach(i => i.classList.remove('is-active')); item.classList.add('is-active'); }); }); (function () { window.DATIS_QTY_INIT = true; function num(v) { const n = parseFloat(v); return Number.isFinite(n) ? n : NaN; } function getInput(root) { return ( root.querySelector("input.qty") || root.querySelector('input[name="quantity"]') || root.querySelector('input[type="number"]') ); } function clamp(v, min, max) { if (Number.isFinite(max)) v = Math.min(v, max); if (Number.isFinite(min)) v = Math.max(v, min); return v; } document.addEventListener("click", function (e) { const btn = e.target.closest(".qty-btn"); if (!btn) return; const root = btn.closest("[data-qty-control]"); if (!root) return; const input = getInput(root); if (!input || input.disabled) return; e.preventDefault(); const step = Math.max(num(input.step) || 1, 1); const min = input.min === "" ? 1 : num(input.min); const max = input.max === "" ? NaN : num(input.max); const current = num(input.value); const base = Number.isFinite(current) ? current : (Number.isFinite(min) ? min : 1); const next = btn.classList.contains("qty-plus") ? (base + step) : (base - step); input.value = clamp(next, Number.isFinite(min) ? min : 1, max); input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true })); }); })(); // Category tabs swiper (your original) (function () { function initDatisCatTabsSwiper() { const swiperEl = document.querySelector(".datis-cat-products-swiper"); if (!swiperEl || typeof Swiper === "undefined") return; const wrapper = swiperEl.querySelector(".swiper-wrapper"); const tabs = document.querySelectorAll(".datis-cat-tab"); const sw = new Swiper(".datis-cat-products-swiper", { slidesPerView: 2, spaceBetween: 12, breakpoints: { 0: { slidesPerView: 1.2 }, 576: { slidesPerView: 2 }, 992: { slidesPerView: 4 }, }, }); async function loadTerm(termId) { if (!termId) return; swiperEl.classList.add("is-loading"); const body = new URLSearchParams(); body.set("action", "datis_cat_products"); body.set("nonce", DATIS_CAT_TABS.nonce); body.set("term_id", termId); try { const res = await fetch(DATIS_CAT_TABS.ajaxUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: body.toString(), }); const json = await res.json(); if (!json || !json.success) return; wrapper.innerHTML = json.data.slides_html || ""; sw.update(); sw.slideTo(0); } catch (e) { // silent } finally { swiperEl.classList.remove("is-loading"); } } tabs.forEach((btn) => { btn.addEventListener("click", async () => { tabs.forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); const termId = btn.getAttribute("data-term-id"); await loadTerm(termId); }); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initDatisCatTabsSwiper); } else { initDatisCatTabsSwiper(); } })(); }); /* ============================================================================ * ARCHIVE: Price Filter Slider (Rounded step for IRR/Toman) * ========================================================================== */ (function () { /** * Format archive price using DATIS_WC config (Woo settings). * - Returns plain text * - No currency symbol here (you show symbol in HTML separately) */ function formatArchivePrice(amount) { if (typeof DATIS_WC === 'undefined') return String(amount); return datisFormatWooPrice(amount, DATIS_WC, false); } /** * Snap a number to nearest step (e.g. 100000). * @param {number} v * @param {number} step * @returns {number} */ function snapToStep(v, step) { const n = Number(v); const s = Number(step) || 1; if (!Number.isFinite(n) || !Number.isFinite(s) || s <= 0) return n; return Math.round(n / s) * s; } function initPriceFilter() { document.querySelectorAll('[data-price-filter]').forEach((root) => { const minRange = root.querySelector('.datis-range-min'); const maxRange = root.querySelector('.datis-range-max'); const fill = root.querySelector('[data-fill]'); const minText = root.querySelector('[data-min-text]'); const maxText = root.querySelector('[data-max-text]'); const minHidden = root.querySelector('[data-min-hidden]'); const maxHidden = root.querySelector('[data-max-hidden]'); if (!minRange || !maxRange || !fill || !minHidden || !maxHidden) return; const min = Number(root.getAttribute('data-min') || 0); const max = Number(root.getAttribute('data-max') || 0); // Step should come from the input (best practice). // Example in PHP: step="100000" const step = Number(minRange.getAttribute('step') || 100000); // Guard bounds if (!Number.isFinite(min) || !Number.isFinite(max) || max <= 0 || max < min) return; const clamp = (v) => Math.max(min, Math.min(max, v)); const paint = () => { let a = clamp(Number(minRange.value)); let b = clamp(Number(maxRange.value)); // Snap values to step (IRR/Toman rounding) a = snapToStep(a, step); b = snapToStep(b, step); // Prevent crossing if (a > b) { const t = a; a = b; b = t; } // Clamp again after snap a = clamp(a); b = clamp(b); // Sync inputs minRange.value = a; maxRange.value = b; // Labels (plain text formatted with separators/decimals) if (minText) minText.textContent = formatArchivePrice(a); if (maxText) maxText.textContent = formatArchivePrice(b); // Hidden values for GET submit (IMPORTANT: use snapped values) minHidden.value = String(a); maxHidden.value = String(b); // Fill bar const right = ((a - min) / (max - min || 1)) * 100; const left = ((b - min) / (max - min || 1)) * 100; fill.style.right = right + '%'; fill.style.width = (left - right) + '%'; // z-index tweak when thumbs overlap if (Math.abs(b - a) < step) { minRange.style.zIndex = '3'; maxRange.style.zIndex = '2'; } else { minRange.style.zIndex = '2'; maxRange.style.zIndex = '3'; } }; minRange.addEventListener('input', paint); maxRange.addEventListener('input', paint); // Initial paint paint(); }); } document.addEventListener('DOMContentLoaded', initPriceFilter); })(); /* ============================================================================ * The rest of your scripts (unchanged) * ========================================================================== */ document.addEventListener('DOMContentLoaded', function () { const list = document.querySelector('.notice-list'); if (!list) return; const items = list.querySelectorAll('.notice-item'); function closeItem(item) { item.classList.remove('is-open'); const btn = item.querySelector('.notice-item-toggle'); const panel = item.querySelector('.notice-panel'); if (!btn || !panel) return; btn.setAttribute('aria-expanded', 'false'); panel.setAttribute('aria-hidden', 'true'); panel.style.height = '0px'; } function openItem(item) { item.classList.add('is-open'); const btn = item.querySelector('.notice-item-toggle'); const panel = item.querySelector('.notice-panel'); const inner = item.querySelector('.notice-panel-inner'); if (!btn || !panel || !inner) return; btn.setAttribute('aria-expanded', 'true'); panel.setAttribute('aria-hidden', 'false'); panel.style.height = '100%'; } items.forEach((item) => { const btn = item.querySelector('.notice-item-toggle'); if (!btn) return; btn.addEventListener('click', () => { const isOpen = item.classList.contains('is-open'); items.forEach((i) => { if (i !== item) closeItem(i); }); if (isOpen) closeItem(item); else openItem(item); }); }); window.addEventListener('resize', () => { const open = list.querySelector('.notice-item.is-open'); if (!open) return; const panel = open.querySelector('.notice-panel'); const inner = open.querySelector('.notice-panel-inner'); if (panel && inner) panel.style.height = inner.scrollHeight + 'px'; }); }); jQuery(function ($) { if (typeof DatisWishlist === "undefined") return; $(document).on("click", "[data-datis-wishlist-btn='1']", function (e) { e.preventDefault(); e.stopPropagation(); var $btn = $(this); if ($btn.data("loading")) return; var productId = parseInt($btn.data("product-id"), 10); if (!productId) return; $btn.data("loading", 1).addClass("is-loading"); $.ajax({ url: DatisWishlist.ajax_url, method: "POST", dataType: "json", data: { action: "datis_toggle_wishlist", nonce: DatisWishlist.nonce, product_id: productId } }) .done(function (res) { if (!res || !res.success) { if (res && res.data && res.data.message === "login_required" && res.data.login_url) { alert(DatisWishlist.i18n_login_required || "Login required"); window.location.href = res.data.login_url; return; } alert(DatisWishlist.i18n_error || "Error"); return; } var inWishlist = !!(res.data && parseInt(res.data.in_wishlist, 10) === 1); $btn.toggleClass("is-active", inWishlist); $btn.attr("aria-pressed", inWishlist ? "true" : "false"); }) .fail(function () { alert(DatisWishlist.i18n_error || "Error"); }) .always(function () { $btn.data("loading", 0).removeClass("is-loading"); }); }); }); (function () { var tabs = document.querySelectorAll('.consult-req-tab'); var rows = document.querySelectorAll('.datis-cr-row'); if (!tabs.length || !rows.length) return; function setActive(btn) { tabs.forEach(function (t) { t.classList.remove('is-active'); t.setAttribute('aria-selected', 'false'); }); btn.classList.add('is-active'); btn.setAttribute('aria-selected', 'true'); } function applyFilter(key) { rows.forEach(function (row) { var st = row.getAttribute('data-status') || ''; if (key === 'all' || st === key) { row.style.display = ''; } else { row.style.display = 'none'; } }); } tabs.forEach(function (btn) { btn.addEventListener('click', function () { var key = btn.getAttribute('data-filter') || 'all'; setActive(btn); applyFilter(key); }); }); applyFilter('all'); })();
💾 保存文件
← 返回文件管理器