UNPKG

zz-shopify-components

Version:

Reusable Shopify components for theme projects

468 lines (412 loc) 12.4 kB
class ZZRadioTabsItem extends HTMLElement { constructor() { super(); } connectedCallback() { // 创建radio input const groupName = this.closest('zz-radio-tabs')?.getAttribute('name') || 'option'; // 获取checked属性 - 支持checked和checked="true"两种方式 const checked = this.hasAttribute('checked'); const value = this.getAttribute('value') || ''; const slot = this.innerHTML; this.innerHTML = ` <label class="zz-radio-tabs-wrapper"> <input type="radio" name=${groupName} value=${value} ${checked ? 'checked' : ''}> <div class="zz-radio-tabs-label"> ${slot} </div> </label> `; } } /** * type: default, black 两种风格模式 */ class ZZRadioTabs extends HTMLElement { constructor() { super(); } connectedCallback() { // 获取组件的type属性 const type = this.getAttribute('type') || 'default'; // 给自己加class this.classList.add('zz-radio-tabs'); this.classList.add(`zz-radio-tabs-${type}`); } get value() { const selectedRadio = this.querySelector('input[type="radio"]:checked'); return selectedRadio?.value; } set value(val) { const radio = this.querySelector(`input[type="radio"][value="${val}"]`); if (radio) { radio.checked = true; } } } // 注册自定义元素 customElements.define('zz-radio-tabs-item', ZZRadioTabsItem); customElements.define('zz-radio-tabs', ZZRadioTabs); // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', function () { // 点击视频播放/暂停 // 在 video 标签上添加 class='click-video-play-pause' 即可 (function () { const videoPlayAndPause = document.querySelectorAll( '.click-video-play-pause' ); videoPlayAndPause.forEach((video) => { video.classList.remove('video-play-pause'); video.addEventListener('click', function () { if (this.paused) { this.play(); } else { this.pause(); } }); }); })(); }); /** * 视频按钮+弹窗 */ class ZZVideoBtn extends HTMLElement { constructor() { super(); this.togglePopup = this.togglePopup.bind(this); this.popup = null; } connectedCallback() { this.querySelectorAll('.togglePopup').forEach((el) => { el.addEventListener('click', (event) => { console.log('click'); if (event.target.tagName !== 'VIDEO') { this.togglePopup(); } }); }); this.popup = this.querySelector('.popup'); if (this.popup) { // 将 popup 移动到 body document.body.appendChild(this.popup); } else { console.error('Popup element not found.'); } } disconnectedCallback() { if (this.popup && document.body.contains(this.popup)) { document.body.removeChild(this.popup); // 清理 popup 元素 } } togglePopup() { if (!this.popup) return; const isHidden = this.popup.classList.contains('!tw-hidden'); if (isHidden) { this.showPopup(); } else { this.hidePopup(); } } showPopup() { if (!this.popup) return; this.popup.classList.remove('!tw-hidden'); gsap.fromTo( this.popup, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: 'linear', backdropFilter: 'blur(30px)', onComplete: () => { const video = this.popup.querySelector('video'); if (video) { video.play(); } }, } ); } hidePopup() { if (!this.popup) return; gsap.to(this.popup, { opacity: 0, duration: 0.3, ease: 'linear', onComplete: () => { this.popup.classList.add('!tw-hidden'); const video = this.popup.querySelector('video'); if (video) { video.pause(); } }, }); } } if (!customElements.get('zz-video-button')) { customElements.define('zz-video-button', ZZVideoBtn); } if (!customElements.get('zz-video-popup')) { class ZZVideoPopup extends HTMLElement { constructor() { super(); this.togglePopup = this.togglePopup.bind(this); this.popup = null; } connectedCallback() { this.querySelectorAll('.togglePopup').forEach((el) => { el.addEventListener('click', (event) => { console.log('click'); if (event.target.tagName !== 'VIDEO') { this.togglePopup(); } }); }); this.popup = this.querySelector('.popup'); } disconnectedCallback() { if (this.popup && document.body.contains(this.popup)) { document.body.removeChild(this.popup); // 清理 popup 元素 } } togglePopup() { if (!this.popup) return; if (this.popup) { // 将 popup 移动到 body document.body.appendChild(this.popup); } else { console.error('Popup element not found.'); } const isHidden = this.popup.classList.contains('!tw-hidden'); if (isHidden) { this.showPopup(); } else { this.hidePopup(); } } showPopup() { if (!this.popup) return; this.popup.classList.remove('!tw-hidden'); gsap.fromTo( this.popup, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: 'linear', backdropFilter: 'blur(30px)', onComplete: () => { const videos = this.popup.querySelectorAll('video'); videos.forEach(video => { if (window.getComputedStyle(video).display !== 'none') { video.play(); } }); }, } ); } hidePopup() { if (!this.popup) return; gsap.to(this.popup, { opacity: 0, duration: 0.3, ease: 'linear', onComplete: () => { this.popup.classList.add('!tw-hidden'); const videos = this.popup.querySelectorAll('video'); videos.forEach(video => { if (video) { video.pause(); } }); }, }); if (this.popup && document.body.contains(this.popup)) { document.body.removeChild(this.popup); // 清理 popup 元素 } } } customElements.define('zz-video-popup', ZZVideoPopup); } /** * Toast 组件 */ (function () { let toastEl = null; let hideTimer = null; const toastIcon = { error: `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_19382_20972)"> <circle cx="9" cy="9" r="9" fill="#FF4D4F"/> <path d="M6.24231 6.17188L11.8992 11.8287" stroke="white" stroke-linecap="round"/> <path d="M6.24231 11.8281L11.8992 6.17127" stroke="white" stroke-linecap="round"/> </g> <defs> <clipPath id="clip0_19382_20972"> <rect width="18" height="18" fill="white"/> </clipPath> </defs> </svg> `, success: `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_19382_20921)"> <circle cx="9" cy="9" r="9" fill="#5BC726"/> <path d="M5 9L7.82843 11.8284L12.7782 6.87868" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> </g> <defs> <clipPath id="clip0_19382_20921"> <rect width="18" height="18" fill="white"/> </clipPath> </defs> </svg> `, warning: `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_19382_20989)"> <circle cx="9" cy="9" r="9" fill="#FAAD14"/> <path d="M8.99994 5L8.99994 10" stroke="white" stroke-linecap="round"/> <circle cx="9.00001" cy="12.8" r="0.8" fill="white"/> </g> <defs> <clipPath id="clip0_19382_20989"> <rect width="18" height="18" fill="white"/> </clipPath> </defs> </svg> `, }; function createToast() { const layer = document.createElement('div'); layer.className = 'zz-toast-layer'; layer.setAttribute('aria-live', 'assertive'); layer.setAttribute('aria-atomic', 'true'); const box = document.createElement('div'); box.className = 'zz-toast-box'; box.setAttribute('role', 'alert'); const msg = document.createElement('span'); msg.className = 'zz-toast-msg'; const icon = document.createElement('span'); icon.className = 'zz-toast-icon'; box.appendChild(icon); box.appendChild(msg); layer.appendChild(box); document.body.appendChild(layer); return { layer, box, msg, icon }; } /** * 显示错误 Toast * @param {string} message - 要显示的错误文字 * @param {{duration?:number,type?:string}} [opts] */ function zzShowToast(message, opts = {}) { if (!toastEl) { toastEl = createToast(); } // 更新文本 toastEl.msg.textContent = message ?? ''; if (opts.type) { toastEl.icon.innerHTML = toastIcon[opts.type]; } else { toastEl.icon.innerHTML = ''; } // 重新插入到 body 末尾,保证在最上层 document.body.appendChild(toastEl.layer); // 显示动画 requestAnimationFrame(() => { toastEl.box.classList.add('show'); }); // 清理上一次的计时器 clearTimeout(hideTimer); const duration = Math.max(500, Number(opts.duration || 2000)); hideTimer = setTimeout(() => { toastEl.box.classList.remove('show'); // 动画结束后移除节点(留层以复用 DOM 也行,这里直接移除) setTimeout(() => { toastEl.layer.remove(); toastEl = null; }, 200); }, duration); } // 暴露到全局 window.zzShowToast = zzShowToast; })(); (function () { let scrollTop = 0; function isSafari() { const ua = navigator.userAgent; // 包含 Safari / WebKit,且不包含 Chrome / CriOS / FxiOS / Android return /Safari/.test(ua) && /AppleWebKit/.test(ua) && !/CriOS|FxiOS|Chrome|Edg|OPR/.test(ua); } function isSafari26() { const ua = navigator.userAgent; if (!isSafari()) return false; // 检查 “Version/26.0” 或类似标识 return /Version\/26\./.test(ua); } function lockBodyScroll() { if (isSafari26()) { scrollTop = window.scrollY; document.documentElement.classList.add('zz-global-noscroll'); } else { document.body.style.overflow = 'hidden'; } } function unlockBodyScroll() { if (isSafari26()) { document.documentElement.classList.remove('zz-global-noscroll'); document.documentElement.style.scrollBehavior = 'auto'; window.scrollTo(0, scrollTop); requestAnimationFrame(() => { document.documentElement.style.scrollBehavior = ''; }); } else { document.body.style.overflow = ''; } } window.zzLockBodyScroll = lockBodyScroll; window.zzUnlockBodyScroll = unlockBodyScroll; })(); /**获取当前显示的媒体元素 */ function getVisibleDisplayMedias(container) { const videos = container.querySelectorAll('video'); const video = Array.from(videos).find((video) => { const style = window.getComputedStyle(video); return style.display !== 'none'; }); if (video) { return { video, type: 'video', }; } else { const images = container.querySelectorAll('img'); const image = Array.from(images).find((image) => { const style = window.getComputedStyle(image); return style.display !== 'none'; }); if (image) { return { image, type: 'image', }; } } return null; } // component 统一的初始化入口 document.addEventListener('DOMContentLoaded', (event) => { const isDesktop = window.innerWidth > 1023; if (!isDesktop) { const switchCard = document.querySelectorAll('.product-switch-card'); if (switchCard.length > 0) { setInterval(() => { switchCard.forEach(card => { card.classList.toggle('switchCard'); }); }, 3000); } } });