/** * AKSRT Runtime Infrastructure * * Unified core primitives for the entire frontend: * aksrt.tbquire(deps, cb) — lazy script/CSS loader * aksrt.notyf(msg, opts) — unified toast (same-ID update) * aksrt.confirm(msg, opts) — i18n confirm dialog * aksrt.refreshModal(opts) — remote content modal * aksrt.scrollTopTo(target, off) — smart scroll with header offset * aksrt.loading(el, show) — element-level loading overlay * aksrt.Drawer(opts) — mobile fullscreen drawer + route * aksrt.route(url, title) — push/replace URL + title * * Declarative interactions (event delegation on body): * [data-toggle-class] — toggle class on target * [data-close] — close target element * [data-onclick] — trigger click on another element * [data-confirm] — confirm before action * [data-action] — universal AJAX action button (like/follow/checkin) * [ajax-action] — general AJAX button with modal response * * Body scroll direction bus: * body.scroll-down / body.scroll-up (threshold 100px) * * @package AKSRT * @since 5.0.0 */ (function () { 'use strict'; var aksrt = window.aksrt || {}; window.aksrt = aksrt; // ══════════════════════════════════════════════════════════════ // §10.11 — tbquire: lazy script/CSS loader // ══════════════════════════════════════════════════════════════ var _loadedScripts = {}; var _loadingPromises = {}; function loadSingleScript(src) { if (_loadedScripts[src]) return Promise.resolve(); if (_loadingPromises[src]) return _loadingPromises[src]; _loadingPromises[src] = new Promise(function (resolve, reject) { var isCSS = /\.css(\?|$)/i.test(src); var el; if (isCSS) { el = document.createElement('link'); el.rel = 'stylesheet'; el.href = src; } else { el = document.createElement('script'); el.src = src; el.async = true; } el.onload = function () { _loadedScripts[src] = true; delete _loadingPromises[src]; resolve(); }; el.onerror = function () { delete _loadingPromises[src]; reject(new Error('Failed to load: ' + src)); }; (document.head || document.documentElement).appendChild(el); }); return _loadingPromises[src]; } aksrt.tbquire = function (deps, callback) { if (typeof deps === 'string') deps = [deps]; if (!Array.isArray(deps) || !deps.length) { if (typeof callback === 'function') callback(); return Promise.resolve(); } var resolvedDeps = deps.map(function (dep) { if (/^https?:\/\/|^\//.test(dep)) return dep; var map = (window.aksrtData && aksrtData.tbquireMap) || {}; return map[dep] || dep; }); var p = Promise.all(resolvedDeps.map(loadSingleScript)); if (typeof callback === 'function') { p.then(callback).catch(function (err) { console.error('[AKSRT tbquire]', err); }); } return p; }; // ══════════════════════════════════════════════════════════════ // §10.10 — notyf: unified toast with same-ID update // ══════════════════════════════════════════════════════════════ var _notyfContainer = null; var _notyfMap = {}; var NOTYF_DURATION = 5000; function getNotyfContainer() { if (_notyfContainer && _notyfContainer.parentNode) return _notyfContainer; _notyfContainer = document.createElement('div'); _notyfContainer.className = 'aksrt-notyf'; _notyfContainer.setAttribute('role', 'alert'); _notyfContainer.setAttribute('aria-live', 'polite'); document.body.appendChild(_notyfContainer); return _notyfContainer; } function createNotyfItem(msg, opts) { var el = document.createElement('div'); el.className = 'aksrt-notyf__item aksrt-notyf--' + (opts.type || 'info'); if (opts.type === 'load') el.classList.add('aksrt-notyf--loading'); var iconMap = { success: '', danger: '', error: '', warning: '', load: '', info: '' }; el.innerHTML = '' + (iconMap[opts.type] || iconMap.info) + '' + '' + escapeHtml(msg) + '' + ''; el.querySelector('.aksrt-notyf__close').addEventListener('click', function () { removeNotyf(el, opts.id); }); return el; } function removeNotyf(el, id) { el.classList.add('aksrt-notyf--out'); setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300); if (id && _notyfMap[id]) { clearTimeout(_notyfMap[id].timer); delete _notyfMap[id]; } } aksrt.notyf = function (msg, opts) { if (typeof opts === 'string') opts = { type: opts }; opts = opts || {}; var type = opts.type || 'info'; var duration = type === 'load' ? 0 : (opts.duration || NOTYF_DURATION); var id = opts.id || null; var container = getNotyfContainer(); if (id && _notyfMap[id]) { var existing = _notyfMap[id]; clearTimeout(existing.timer); existing.el.className = 'aksrt-notyf__item aksrt-notyf--' + type; existing.el.querySelector('.aksrt-notyf__text').textContent = msg; var iconSpan = existing.el.querySelector('.aksrt-notyf__icon'); var iconMap2 = { success: '', danger: '', error: '', warning: '', load: '', info: '' }; iconSpan.innerHTML = iconMap2[type] || iconMap2.info; if (duration > 0) { existing.timer = setTimeout(function () { removeNotyf(existing.el, id); }, duration); } return existing.el; } var el = createNotyfItem(msg, { type: type, id: id }); container.appendChild(el); requestAnimationFrame(function () { el.classList.add('aksrt-notyf--in'); }); var timer = null; if (duration > 0) { timer = setTimeout(function () { removeNotyf(el, id); }, duration); } if (id) { _notyfMap[id] = { el: el, timer: timer }; } return el; }; aksrt.notyf.close = function (id) { if (_notyfMap[id]) removeNotyf(_notyfMap[id].el, id); }; // Backward compat: wire window.aksrtToast to notyf window.aksrtToast = function (msg, type) { aksrt.notyf(msg, { type: type || 'info', duration: 2200 }); }; // Alias used by several modules that call aksrt.toast(...) aksrt.toast = function (msg, type) { return aksrt.notyf(msg, typeof type === 'string' ? { type: type, duration: 2200 } : (type || { duration: 2200 })); }; // ══════════════════════════════════════════════════════════════ // §10.10 — confirm: i18n confirm dialog + [data-confirm] // ══════════════════════════════════════════════════════════════ var _i18n = (window.aksrtData && aksrtData.i18n) || {}; function __(key, fallback) { return _i18n[key] || fallback || key; } aksrt.__ = __; aksrt.confirm = function (msg, opts) { opts = opts || {}; return new Promise(function (resolve) { var overlay = document.createElement('div'); overlay.className = 'aksrt-confirm-overlay'; var dialog = document.createElement('div'); dialog.className = 'aksrt-confirm-dialog'; dialog.setAttribute('role', 'alertdialog'); dialog.setAttribute('aria-modal', 'true'); dialog.innerHTML = '
' + escapeHtml(msg) + '
' + '
' + '' + '' + '
'; overlay.appendChild(dialog); document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('is-open'); }); function close(result) { overlay.classList.remove('is-open'); setTimeout(function () { overlay.remove(); }, 200); resolve(result); } dialog.querySelector('.aksrt-confirm__cancel').addEventListener('click', function () { close(false); }); dialog.querySelector('.aksrt-confirm__ok').addEventListener('click', function () { close(true); }); overlay.addEventListener('click', function (e) { if (e.target === overlay) close(false); }); dialog.querySelector('.aksrt-confirm__ok').focus(); }); }; // ══════════════════════════════════════════════════════════════ // §10.10 — scrollTopTo: smart scroll with header offset // ══════════════════════════════════════════════════════════════ aksrt.scrollTopTo = function (target, extraOffset) { var top = 0; if (typeof target === 'number') { top = target; } else if (target && target.getBoundingClientRect) { top = target.getBoundingClientRect().top + window.pageYOffset; } var navFixed = document.querySelector('body.nav-fixed .header, .navbar.navbar-top'); var headerOffset = navFixed ? (navFixed.offsetHeight + 20) : 0; top = Math.max(0, top - headerOffset - (extraOffset || 0)); var current = window.pageYOffset; if (Math.abs(top - current) < 80) return; window.scrollTo({ top: top, behavior: 'smooth' }); }; // ══════════════════════════════════════════════════════════════ // §10.10 — loading: element-level loading overlay // ══════════════════════════════════════════════════════════════ aksrt.loading = function (el, show) { if (!el) return; if (show === false) { var mask = el.querySelector('.aksrt-loading-mask'); if (mask) { mask.classList.add('aksrt-loading-mask--out'); setTimeout(function () { mask.remove(); }, 300); } return; } if (el.querySelector('.aksrt-loading-mask')) return; var pos = getComputedStyle(el).position; if (pos === 'static') el.style.position = 'relative'; var mask = document.createElement('div'); mask.className = 'aksrt-loading-mask'; mask.innerHTML = '' + '' + '' + ''; el.appendChild(mask); }; // ══════════════════════════════════════════════════════════════ // §10.10 — refreshModal: remote content modal // ══════════════════════════════════════════════════════════════ var _refreshModalEl = null; function getRefreshModalEl() { if (_refreshModalEl && _refreshModalEl.parentNode) return _refreshModalEl; _refreshModalEl = document.createElement('div'); _refreshModalEl.className = 'aksrt-refresh-modal'; _refreshModalEl.innerHTML = '
' + '
' + '
' + '

' + '' + '
' + '
' + '
'; document.body.appendChild(_refreshModalEl); _refreshModalEl.querySelector('.aksrt-refresh-modal__close').addEventListener('click', closeRefreshModal); _refreshModalEl.querySelector('.aksrt-refresh-modal__overlay').addEventListener('click', function (e) { if (e.target === e.currentTarget) closeRefreshModal(); }); return _refreshModalEl; } function closeRefreshModal() { if (!_refreshModalEl) return; _refreshModalEl.classList.remove('is-open', 'from-bottom'); document.body.classList.remove('modal-open'); setTimeout(function () { var body = _refreshModalEl.querySelector('.aksrt-refresh-modal__body'); if (body) body.innerHTML = ''; }, 300); } aksrt.refreshModal = function (opts) { opts = opts || {}; var modal = getRefreshModalEl(); var body = modal.querySelector('.aksrt-refresh-modal__body'); var title = modal.querySelector('.aksrt-refresh-modal__title'); if (opts.title) title.textContent = opts.title; else title.textContent = ''; var isMobile = window.innerWidth < 768; modal.classList.toggle('from-bottom', !!(opts.mobile_from_bottom && isMobile)); if (opts.class) modal.querySelector('.aksrt-refresh-modal__dialog').className = 'aksrt-refresh-modal__dialog ' + opts.class; body.innerHTML = ''; aksrt.loading(body, true); modal.classList.add('is-open'); document.body.classList.add('modal-open'); if (opts.content) { aksrt.loading(body, false); body.innerHTML = opts.content; if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal'); return Promise.resolve(modal); } if (opts.remote) { return fetch(opts.remote, { credentials: 'same-origin' }) .then(function (r) { return r.text(); }) .then(function (html) { aksrt.loading(body, false); body.innerHTML = html; if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal'); return modal; }) .catch(function (err) { aksrt.loading(body, false); body.innerHTML = '

' + escapeHtml(err.message) + '

'; return modal; }); } if (opts.action) { var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; var params = new URLSearchParams(); params.set('action', opts.action); if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce); if (opts.data) { Object.keys(opts.data).forEach(function (k) { params.set(k, opts.data[k]); }); } return fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { aksrt.loading(body, false); if (json && json.data && json.data.html) { body.innerHTML = json.data.html; } else if (json && json.data && json.data.show_modal) { body.innerHTML = json.data.show_modal; } else { body.innerHTML = '

' + escapeHtml(json.data && json.data.message || '') + '

'; } if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal'); return modal; }) .catch(function (err) { aksrt.loading(body, false); body.innerHTML = '

' + escapeHtml(err.message) + '

'; return modal; }); } aksrt.loading(body, false); return Promise.resolve(modal); }; aksrt.refreshModal.close = closeRefreshModal; // ══════════════════════════════════════════════════════════════ // §10.11 — Drawer: mobile fullscreen slide-in + route // ══════════════════════════════════════════════════════════════ aksrt.Drawer = function (opts) { opts = opts || {}; var drawerEl = document.createElement('div'); drawerEl.className = 'aksrt-drawer' + (opts.class ? ' ' + opts.class : ''); drawerEl.innerHTML = '
' + '' + '' + escapeHtml(opts.title || '') + '' + '
' + '
'; var body = drawerEl.querySelector('.aksrt-drawer__body'); if (opts.content) { body.innerHTML = opts.content; } else if (opts.remote) { aksrt.loading(body, true); fetch(opts.remote, { credentials: 'same-origin' }) .then(function (r) { return r.text(); }) .then(function (html) { aksrt.loading(body, false); body.innerHTML = html; if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'drawer'); }) .catch(function () { aksrt.loading(body, false); }); } document.body.appendChild(drawerEl); document.body.classList.add('drawer-open'); requestAnimationFrame(function () { drawerEl.classList.add('is-open'); }); if (opts.route) { aksrt.route(opts.route, opts.title); } function close() { drawerEl.classList.remove('is-open'); document.body.classList.remove('drawer-open'); setTimeout(function () { drawerEl.remove(); }, 300); if (opts.route) { window.history.back(); } if (typeof opts.onClose === 'function') opts.onClose(); } drawerEl.querySelector('.aksrt-drawer__back').addEventListener('click', close); // minitouch right-swipe close if (window.aksrt.minitouch) { aksrt.minitouch(drawerEl, { direction: 'right', onEnd: close }); } return { el: drawerEl, body: body, close: close }; }; // ══════════════════════════════════════════════════════════════ // §10.11 — route: tab route + popstate // ══════════════════════════════════════════════════════════════ aksrt.route = function (url, title) { if (!window.history || !history.pushState) return; history.pushState({ aksrt: true }, title || '', url); if (title) document.title = title; }; aksrt.routeReplace = function (url, title) { if (!window.history || !history.replaceState) return; history.replaceState({ aksrt: true }, title || '', url); if (title) document.title = title; }; // Tab route: elements with [data-route] push URL on activation window.addEventListener('popstate', function (e) { var routeTabs = document.querySelectorAll('[data-route]'); if (!routeTabs.length) return; var path = window.location.pathname + window.location.search; routeTabs.forEach(function (tab) { var tabRoute = tab.getAttribute('data-route'); var isMatch = path === tabRoute || path.indexOf(tabRoute) === 0; tab.classList.toggle('is-active', isMatch); var target = tab.getAttribute('data-route-target'); if (target) { var panel = document.querySelector(target); if (panel) panel.classList.toggle('is-active', isMatch); } }); }); // ══════════════════════════════════════════════════════════════ // §10.2 — Unified body scroll direction bus // ══════════════════════════════════════════════════════════════ var _lastScrollTop = 0; var _scrollTicking = false; var SCROLL_BUS_THRESHOLD = 100; function updateScrollBus() { var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var body = document.body; if (scrollTop > SCROLL_BUS_THRESHOLD) { body.classList.add('body-scroll'); } else { body.classList.remove('body-scroll'); } if (scrollTop > _lastScrollTop + 10) { body.classList.add('scroll-down'); body.classList.remove('scroll-up'); } else if (scrollTop < _lastScrollTop - 10) { body.classList.remove('scroll-down'); body.classList.add('scroll-up'); } _lastScrollTop = Math.max(0, scrollTop); _scrollTicking = false; } window.addEventListener('scroll', function () { if (!_scrollTicking) { _scrollTicking = true; requestAnimationFrame(updateScrollBus); } }, { passive: true }); // ══════════════════════════════════════════════════════════════ // §10.11 — Declarative interactions (event delegation) // ══════════════════════════════════════════════════════════════ document.addEventListener('click', function (e) { var target = e.target.closest('[data-toggle-class]'); if (target) { e.preventDefault(); var cls = target.getAttribute('data-toggle-class'); var sel = target.getAttribute('data-target') || target.getAttribute('data-toggle-target'); var el = sel ? document.querySelector(sel) : target; if (el && cls) el.classList.toggle(cls); return; } target = e.target.closest('[data-close]'); if (target) { e.preventDefault(); var sel2 = target.getAttribute('data-close'); var el2 = sel2 ? document.querySelector(sel2) : target.parentElement; if (el2) { el2.classList.remove('is-open', 'is-active', 'is-visible'); el2.hidden = true; } return; } target = e.target.closest('[data-onclick]'); if (target) { e.preventDefault(); var sel3 = target.getAttribute('data-onclick'); var el3 = document.querySelector(sel3); if (el3) el3.click(); return; } }); // [data-confirm] — intercept submission document.addEventListener('click', function (e) { var target = e.target.closest('[data-confirm]'); if (!target) return; var msg = target.getAttribute('data-confirm'); if (!msg) return; e.preventDefault(); e.stopPropagation(); aksrt.confirm(msg).then(function (ok) { if (!ok) return; target.removeAttribute('data-confirm'); target.click(); target.setAttribute('data-confirm', msg); }); }, true); // ══════════════════════════════════════════════════════════════ // §10.11 — [data-action] / [ajax-action] universal buttons // ══════════════════════════════════════════════════════════════ // Only these data-action values are network AJAX. UI-only actions // (toggle-theme, editor toolbar, download, etc.) must not hit admin-ajax. var DATA_ACTION_AJAX = { like: true, favorite: true, follow: true, checkin: true }; document.addEventListener('click', function (e) { // [data-action] — like/follow/checkin with localStorage de-dup var btn = e.target.closest('[data-action]'); if (btn) { var dataAction = btn.getAttribute('data-action') || ''; if (!DATA_ACTION_AJAX[dataAction]) { // Leave non-AJAX data-action handlers (theme toggle, toolbar, download) alone. return; } e.preventDefault(); handleDataAction(btn); return; } // [ajax-action] — general AJAX button btn = e.target.closest('[ajax-action]'); if (btn) { e.preventDefault(); handleAjaxAction(btn); return; } }); function handleDataAction(btn) { if (btn.classList.contains('is-loading')) return; var action = btn.getAttribute('data-action'); if (!action || !DATA_ACTION_AJAX[action]) return; var id = btn.getAttribute('data-id') || ''; var storageKey = 'aksrt_action_' + action + '_' + id; if (action === 'like' || action === 'favorite') { try { if (localStorage.getItem(storageKey) && !btn.classList.contains('is-active')) { aksrt.notyf(__('already_done', '已操作'), 'warning'); return; } } catch (ex) { /* storage unavailable */ } } btn.classList.add('is-loading'); var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; if (!(window.aksrtData && aksrtData.nonce)) { btn.classList.remove('is-loading'); aksrt.notyf(__('action_failed', '操作失败'), 'danger'); return; } var params = new URLSearchParams(); params.set('action', 'aksrt_' + action); params.set('nonce', aksrtData.nonce); params.set('id', id); var extraData = btn.getAttribute('data-action-data'); if (extraData) { try { var ed = JSON.parse(extraData); Object.keys(ed).forEach(function (k) { params.set(k, ed[k]); }); } catch (ex) { /* ignore */ } } fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { btn.classList.remove('is-loading'); if (json && json.success) { var isActive = !!json.data && !!json.data.active; btn.classList.toggle('is-active', isActive); var countEl = btn.querySelector('[data-count]') || btn.querySelector('.action-count'); if (countEl && json.data && json.data.count !== undefined) { countEl.textContent = String(json.data.count); } try { if (isActive) localStorage.setItem(storageKey, '1'); else localStorage.removeItem(storageKey); } catch (ex) { /* */ } if (json.data && json.data.message) { aksrt.notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; } } else { aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); aksrt.notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } function handleAjaxAction(btn) { if (btn.classList.contains('is-loading')) return; btn.classList.add('is-loading'); var action = btn.getAttribute('ajax-action'); var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; var params = new URLSearchParams(); params.set('action', action); if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce); var form = btn.closest('form'); if (form) { new FormData(form).forEach(function (v, k) { params.set(k, v); }); } fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { btn.classList.remove('is-loading'); if (json && json.success) { if (json.data && json.data.show_modal) { aksrt.refreshModal({ content: json.data.show_modal, mobile_from_bottom: true }); } else if (json.data && json.data.message) { aksrt.notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; } if (json.data && json.data.reload) { window.location.reload(); } } else { aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); aksrt.notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } // ══════════════════════════════════════════════════════════════ // §10.11 — Ripple (broad coverage) // ══════════════════════════════════════════════════════════════ var RIPPLE_SELECTORS = '.toggle-radius,.but-ripple,.but,.btn,.btn-primary,.btn-ghost,.btn-outline,.item-thumbnail,.menu-item>a,.aksrt-ripple,.yiyan-box,.aksrt-card'; document.addEventListener('click', function (e) { var target = e.target.closest(RIPPLE_SELECTORS); if (!target) return; // Markup opt-out (header publish uses class="nowave") + nav CTA chips if (target.classList.contains('nowave') || target.closest('.nowave')) return; if (target.classList.contains('navbar-cta') || target.closest('.navbar-cta')) return; if (target.closest('.navbar-top .navbar-but, .navbar-top .navbar-vip, .navbar-but-cluster')) return; if (target.querySelector('.aksrt-wave')) return; var rect = target.getBoundingClientRect(); var size = Math.max(rect.width, rect.height) * 2; var x = e.clientX - rect.left - size / 2; var y = e.clientY - rect.top - size / 2; var wave = document.createElement('span'); wave.className = 'aksrt-wave'; // Must be absolute — without it the huge span expands the button box ("变长"). wave.style.cssText = 'position:absolute;pointer-events:none;border-radius:50%;background:rgba(255,255,255,0.35);width:' + size + 'px;height:' + size + 'px;left:' + x + 'px;top:' + y + 'px;transform:scale(0);animation:aksrtWaveRipple .6s ease-out forwards;'; var cs = getComputedStyle(target); if (cs.position === 'static') target.style.position = 'relative'; if (cs.overflow === 'visible') target.style.overflow = 'hidden'; target.appendChild(wave); setTimeout(function () { if (wave.parentNode) wave.parentNode.removeChild(wave); }, 600); }); // ══════════════════════════════════════════════════════════════ // §10.11 — auto_fun: extended rehydration coverage // ══════════════════════════════════════════════════════════════ aksrt.autoFun = function (root) { root = root || document; if (window.AksrtFrontend) { AksrtFrontend.rehydrate(root, 'ajax'); } // Tooltips root.querySelectorAll('[data-tooltip]').forEach(function (el) { if (el.dataset.tooltipBound) return; el.dataset.tooltipBound = '1'; el.addEventListener('mouseenter', function () { var tip = document.createElement('div'); tip.className = 'aksrt-tooltip'; tip.textContent = el.getAttribute('data-tooltip'); document.body.appendChild(tip); var rect = el.getBoundingClientRect(); tip.style.left = (rect.left + rect.width / 2 - tip.offsetWidth / 2) + 'px'; tip.style.top = (rect.top - tip.offsetHeight - 8 + window.pageYOffset) + 'px'; el._aksrtTooltip = tip; }); el.addEventListener('mouseleave', function () { if (el._aksrtTooltip) { el._aksrtTooltip.remove(); el._aksrtTooltip = null; } }); }); // Lazy images root.querySelectorAll('img.aksrt-lazy[data-src]').forEach(function (img) { if (img.classList.contains('aksrt-loaded')) return; if ('IntersectionObserver' in window) { var obs = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; img.src = img.dataset.src; img.classList.remove('aksrt-lazy'); img.classList.add('aksrt-loaded'); obs.unobserve(img); }); }, { rootMargin: '200px' }); obs.observe(img); } else { img.src = img.dataset.src; img.classList.remove('aksrt-lazy'); img.classList.add('aksrt-loaded'); } }); // Swiper re-init if (typeof Swiper !== 'undefined') { root.querySelectorAll('[data-swiper-config]:not(.swiper-initialized)').forEach(function (el) { try { var cfg = JSON.parse(el.dataset.swiperConfig || '{}'); new Swiper(el, cfg); } catch (ex) { /* */ } }); } // Countdowns root.querySelectorAll('[data-countdown]').forEach(function (el) { if (el.dataset.countdownBound) return; el.dataset.countdownBound = '1'; var end = parseInt(el.getAttribute('data-countdown'), 10); if (!end) return; var iv = setInterval(function () { var left = Math.max(0, end - Math.floor(Date.now() / 1000)); if (left <= 0) { clearInterval(iv); el.textContent = '00:00'; return; } var m = Math.floor(left / 60); var s = left % 60; el.textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; }, 1000); }); }; // ══════════════════════════════════════════════════════════════ // §10.11 — minitouch: general direction gesture // ══════════════════════════════════════════════════════════════ aksrt.minitouch = function (el, opts) { opts = opts || {}; var direction = opts.direction || 'any'; var startX = 0, startY = 0, currentX = 0, currentY = 0; var active = false; el.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; startX = e.touches[0].clientX; startY = e.touches[0].clientY; currentX = startX; currentY = startY; active = true; if (typeof opts.onStart === 'function') { var stop = opts.onStart({ x: startX, y: startY, el: el }); if (stop === false) active = false; } }, { passive: true }); el.addEventListener('touchmove', function (e) { if (!active) return; currentX = e.touches[0].clientX; currentY = e.touches[0].clientY; var dx = currentX - startX; var dy = currentY - startY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 10) return; var angle = Math.atan2(dy, dx) * 180 / Math.PI; var dir = getDirection(angle); if (direction !== 'any' && dir !== direction) { if (typeof opts.stop === 'function' && opts.stop({ direction: dir, dx: dx, dy: dy })) { active = false; return; } } if (typeof opts.onIng === 'function') { opts.onIng({ dx: dx, dy: dy, direction: dir, distance: dist, el: el }); } }, { passive: true }); el.addEventListener('touchend', function () { if (!active) return; active = false; var dx = currentX - startX; var dy = currentY - startY; var dist = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx) * 180 / Math.PI; var dir = getDirection(angle); if (typeof opts.onEnd === 'function') { opts.onEnd({ dx: dx, dy: dy, direction: dir, distance: dist, el: el }); } }, { passive: true }); }; function getDirection(angle) { if (angle >= -45 && angle < 45) return 'right'; if (angle >= 45 && angle < 135) return 'bottom'; if (angle >= -135 && angle < -45) return 'top'; return 'left'; } // ══════════════════════════════════════════════════════════════ // Helpers // ══════════════════════════════════════════════════════════════ function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ── DOMContentLoaded ── function initRuntime() { updateScrollBus(); aksrt.autoFun(document); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initRuntime); } else { initRuntime(); } })();