/* * flcart-utils.js — Flannel CART用 汎用ユーティリティ(jQueryベース) * * 含まれる機能: * - Cookie操作(JSON対応) * - モーダル表示(外部HTMLテンプレート読込) * - 画面更新ヘルパー * - Ajax通信ラッパー(CSRF対応、重複リクエスト抑止) * - 汎用ユーティリティ(debounce, throttle) * * 使用例: * FlCart.cookies.setJSON('cartitem', {...}, {days:1}); * const cfg = FlCart.cookies.getJSON('cartitem') || {}; * FlCart.modal.open('/partials/option-modal.html', {onOpen: () => {...}}); * FlCart.ajax.call('priceQuote', { url:'/api/cart/estimate.php', method:'POST', data: cfg }) * .done(res => FlCart.ui.text('#price', res.total_fmt)) * .fail(xhr => FlCart.ui.toast('取得に失敗しました')); */ // ==================== セッション情報管理 ==================== // セッション情報をFlCart.storageから取得(複数の関数から使用可能) function getSessionData(){ let d = FlCart.storage.get('cart_session', { prefer: 'local' }) || { selected: [], selected_pls: [] }; return d; } // カート情報をFlCart.storageから取得(複数の関数から使用可能) function getSessionCartData(){ let d = FlCart.storage.get('cart_items', { prefer: 'local' }) || { cartitem: [], cartitem_pls: [] }; return d; } // セッション情報をFlCart.storageに保存 function setSessionData(data){ FlCart.storage.set('cart_session', data, { ttlSec: 3600, // 1時間 useLocal: true, useCookie: false, cookieDays: 1/12 }); } //カート情報をFlCart.storageに保存 function setSessionCartData(data){ FlCart.storage.set('cart_items', data, { ttlSec: 3600, // 1時間 useLocal: true, useCookie: false, cookieDays: 1/12 }); } // サーバーからセッション情報を取得して更新 function refreshSessionCartData(callback) { FlCart.ajax.call('getSession', { url: '/_functions/cart/ajax_get_session.php', method: 'POST' }).done(function(response) { if (response.success) { setSessionData(response); updateDisplay(response.itemid, response.prod_id, response.prod_name, response.size_id, response.size_name); } else { //console.error('セッション情報の取得に失敗しました'); } }).fail(function(xhr, status, error) { //console.error('Ajax error:', xhr, status, error); }); } function calc_price(){ let res = getSessionData(); let total_price = 0; Object.keys(res.selected).forEach(function(itemid,value) { if( res.selected[itemid].price != '' && res.selected[itemid].price != undefined ){ total_price += res.selected[itemid].price*res.selected[itemid].qty; $.each(res.selected[itemid]['option'], function(key, item){ total_price = total_price + Number(item.opt_price)*Number(res.selected[itemid].qty); }); } }); return total_price; } function calc_fee_amount(){ let res = getSessionData(); let fee_amount = calc_price(); if( fee_amount <= 0 ){ return "-"; }else{ return format_price(fee_amount); } } function calc_fee_small_amount(itemid){ let res = getSessionData(); let fee_amount = 0; if( res.selected[itemid].price != '' && res.selected[itemid].price != undefined ){ fee_amount += res.selected[itemid].price*res.selected[itemid].qty; $.each(res.selected[itemid]['option'], function(key, item){ fee_amount = fee_amount + Number(item.opt_price)*Number(res.selected[itemid].qty); }); }; if( fee_amount <= 0 ){ return "-"; }else{ return format_price(fee_amount); } } function format_price(price){ price = price*(1+0.1); return "¥" + price.toLocaleString(); } // 最短配送日の表示を更新(cart_index 等) function updateShippingDate(senddate, senddateWeek) { if (!senddate) { return; } const week = senddateWeek || ''; const weekHtml = week ? '(' + week + ')' : ''; $('.shipping-details .shipping-date .date').html(senddate + weekHtml); } function applySenddateResponse(res) { if (res && res.senddate) { updateShippingDate(res.senddate, res.senddate_week); } } function fetchShippingDate(callback) { return FlCart.ajax.call('getSenddate', { url: '/_functions/cart/ajax_get_senddate.php', method: 'POST' }).done(function(res) { if (res && res.success) { applySenddateResponse(res); } if (typeof callback === 'function') { callback(res); } }); } (function (global, $) { if (!$) { throw new Error('flcart-utils は jQuery が必要です'); } const FlCart = global.FlCart || {}; /* ======================= Cookie処理 ======================= */ const cookies = { /** Cookieを設定(文字列値) */ set(name, value, opts = {}) { const days = opts.days ?? 1; const path = opts.path ?? '/'; const samesite = opts.samesite ?? 'Lax'; const secure = opts.secure ?? (location.protocol === 'https:'); const domain = opts.domain ? `; domain=${opts.domain}` : ''; let expires = ''; if (typeof days === 'number') { const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); expires = `; expires=${date.toUTCString()}`; } const secureAttr = secure ? '; Secure' : ''; document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=${path}; SameSite=${samesite}${secureAttr}${domain}`; }, /** Cookieの値を取得(存在しない場合はnull) */ get(name) { const target = name + '='; const parts = document.cookie.split(';'); for (let i = 0; i < parts.length; i++) { let c = parts[i].trim(); if (c.indexOf(target) === 0) { return decodeURIComponent(c.substring(target.length)); } } return null; }, /** Cookieを削除 */ del(name, opts = {}) { const path = opts.path ?? '/'; const domain = opts.domain ? `; domain=${opts.domain}` : ''; const secure = opts.secure ?? (location.protocol === 'https:'); const secureAttr = secure ? '; Secure' : ''; document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; SameSite=Lax${secureAttr}${domain}`; }, /** JSONデータをCookieとして保存 */ setJSON(name, obj, opts = {}) { try { const raw = JSON.stringify(obj); if (raw.length > 3500) console.warn(`[FlCart.cookies] ${name} がCookieサイズ制限を超える可能性があります (~3.5KB)`, obj); this.set(name, raw, opts); } catch (e) { console.error('[FlCart.cookies] JSON化に失敗', e); } }, /** JSON Cookieの読み込み */ getJSON(name) { const raw = this.get(name); if (!raw) return null; try { return JSON.parse(raw); } catch (e) { console.error('[FlCart.cookies] JSON解析に失敗', e); return null; } }, /** カート用Cookieをすべて削除 */ removeAll(opts = {}) { const names = ['cart_session', 'cart_items', 'cartitem', 'cartitem_pls']; names.forEach(name => this.del(name, opts)); } }; /* ======================= モーダル ======================= */ const modal = (function(){ let $backdrop = null, $dialog = null, $content = null, lastActive = null; function ensureElements() { if ($backdrop) return; $backdrop = $('
').css({ position: 'fixed', inset: 0, display: 'none', background: 'rgba(0,0,0,0.4)', zIndex: 9998 }); $dialog = $('').css({ position: 'fixed', maxWidth: 'min(720px, 92vw)', width: 'auto', maxHeight: '90vh', overflow: 'auto', background: '#fff', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', borderRadius: '16px', padding: '16px', zIndex: 9999, boxShadow: '0 10px 30px rgba(0,0,0,0.25)' }); $content = $(''); const $close = $('').css({ position: 'absolute', top: 8, right: 12, border: 'none', background: 'transparent', fontSize: '24px', cursor: 'pointer' }).on('click', close); $dialog.append($close, $content); $('body').append($backdrop, $dialog); // バックドロップクリックやESCキーで閉じる $backdrop.on('click', close); $(document).on('keydown.flcModal', function(e){ if (e.key === 'Escape') close(); }); } function trapFocus(e) { if (!$dialog.is(':visible')) return; const $focusables = $dialog.find('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])').filter(':visible'); if ($focusables.length === 0) return; const first = $focusables[0]; const last = $focusables[$focusables.length - 1]; if (e.shiftKey && e.target === first && e.key === 'Tab') { e.preventDefault(); last.focus(); } else if (!e.shiftKey && e.target === last && e.key === 'Tab') { e.preventDefault(); first.focus(); } } function open(url, { data, method = 'GET', onOpen, onLoaded, onClose } = {}) { ensureElements(); lastActive = document.activeElement; $backdrop.fadeIn(120); $dialog.show(); $dialog.attr('aria-busy', 'true'); $content.empty().append('