zz-shopify-components
Version:
Reusable Shopify components for theme projects
468 lines (412 loc) • 12.4 kB
JavaScript
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);
}
}
});