Cookies
Przejdź do treści
Szukaj
Szukaj
Konto
Koszyk
NOWOŚCI
RAJSTOPY I POŃCZOCHY
Pokaż linki
RAJSTOPY
Pokaż linki
Rajstopy gładkie
Rajstopy wzorzyste
Rajstopy funkcyjne
Rajstopy erotyczne
Kabaretki
Rajstopy ślubne
Rajstopy na lato
Rajstopy zimowe
Rajstopy ciążowe
Rajstopy plus size
NA OTARCIA
POŃCZOCHY
Pokaż linki
Pończochy samonośne
Pończochy do pasa
Pończochy ślubne
PODKOLANÓWKI
RAJSTOPY DZIECIĘCE
WIELOPAKI
SKARPETKI
Pokaż linki
Skarpetki damskie
Skarpetki męskie
Skarpetki dziecięce
Skarpetki stopki
BIELIZNA
Pokaż linki
BIELIZNA
Pokaż linki
Bielizna klasyczna
Bielizna bezszwowa
Bielizna modelująca
Bielizna erotyczna
Bielizna nocna
Bielizna ciążowa
Bielizna ślubna
Bielizna koronkowa
BODY
Pokaż linki
Body basic
Body erotyczne
BIUSTONOSZE
Pokaż linki
Miękkie
Usztywniane
Push-up
Deco-plunge
Triangle
Balkonetki
Topy bezszwowe
Biustonosze na mały biust
Biustonosze na duży biust
AKCESORIA BIELIŹNIANE
BIELIZNA MĘSKA
WIELOPAKI
MAJTKI
Pokaż linki
Majtki bikini
Majtki szorty
Majtki z wysokim stanem
Majtki brazyliany
Majtki bezszwowe
Stringi
PIŻAMY
Pokaż linki
Piżamy dwuczęściowe
Piżamy góra
Piżamy dół
Koszule noce
Szlafroki
ODZIEŻ
Pokaż linki
LEGGINSY
Pokaż linki
Legginsy modelujące
Legginsy z wysokim stanem
Legginsy ciążowe
Legginsy sportowe
ODZIEŻ SPORTOWA
Pokaż linki
Bluzy dresowe
Spodnie dresowe
BODY
Pokaż linki
Body z długim rękawem
Body z krótkim rękawem
Body na ramiączkach
Body bezszwowe
BLUZKI
Pokaż linki
Longsleeve
T-shirty
Topy damskie
Koszulki bezszwowe
Koszule damskie
Golfy damskie
SUKIENKI
SPÓDNICE
SPODNIE
ŻAKIETY I MARYNARKI
STROJE KĄPIELOWE
Pokaż linki
JEDNOCZĘŚCIOWE STROJE KĄPIELOWE
DWUCZĘŚCIOWE STROJE KĄPIELOWE
STROJE KĄPIELOWE PLUS SIZE
BIKINI
Pokaż linki
Góra od bikini
Dół od bikini
Z wysokim stanem
AKCESORIA PLAŻOWE
Pokaż linki
Pareo
Ręczniki
Kapelusze plażowe
ODZIEŻ SPORTOWA
Pokaż linki
ODZIEŻ SPORTOWA DAMSKA
Pokaż linki
Topy i staniki sportowe
Szorty sportowe damskie
Skarpetki sportowe damskie
ODZIEŻ SPORTOWA MĘSKA
Pokaż linki
Koszulki sportowe męskie
Bluzy dresowe męskie
Skarpetki sportowe męskie
ODZIEŻ TERMOAKTYWNA
Pokaż linki
Akcesoria termoaktywne
ZIEŃ x GATTA
SALE
Pokaż linki
BIELIZNA
RAJSTOPY
POŃCZOCHY
SKARPETKI
ODZIEŻ
ODZIEŻ TERMOAKTYWNA
STROJE SPORTOWE
STROJE KĄPIELOWE
Gatta.pl
Czapki i szaliki damskie
Czapki i szaliki damskie
Filtruj
Sortuj wg
Od najnowszych
Polecane
Najlepiej dopasowane
Najlepiej sprzedające się
Ceny rosnąco
Ceny malejąco
Od najstarszych
Od najnowszych
Siatka
Lista
Filtry
Filtry
Zamknij
Kategorie
Sortuj wg
Polecane
Najlepiej dopasowane
Najlepiej sprzedające się
Ceny rosnąco
Ceny malejąco
Od najstarszych
Od najnowszych
Zastosuj
Brak produktów
Powiązane
T-shirty damskie
Body z długim rękawem
Body z krótkim rękawem
Bluzki z długim rękawem
Bluzki z krótkim rękawem
Topy damskie
Bezszwowe body
Body z głębokim dekoltem
Zobacz również
Skarpetki męskie
Wielopaki rajstop i skarpetek
Biustonosze ślubne
Koronkowe biustonosze
Czarne rajstopy
Rajstopy w romby
Dwuczęściowe stroje kąpielowe
Damskie spodnie dresowe
Twój koszyk
Zamknij
Wydaj
49,00 zł
więcej, aby uzyskać
darmową wysyłkę
!
Twoje zamówienie kwalifikuje się do darmowej wysyłki!
Zamknij
Twój koszyk jest pusty
Zacznij zakupy
Dodano do koszyka
Rozmiar:
Kolor:
Ilość:
Zobacz koszyk
body.overflow-hidden { overflow: hidden; } html.lock { overflow: hidden; height: 100%; } wishlist-button:disabled { opacity: 0.5; } wishlist-button, wishlist-button--pdp { display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; z-index: 5; } wishlist-button-pdp.fish-no-label .fish-pdp-label { display: none; } wishlist-button-pdp { padding: 5px; width: fit-content; } wishlist-button-pdp.disabled { opacity: 0.5; pointer-events: none; cursor: not-allowed; } wishlist-button-pdp.disabled.fsb__button { opacity: 0.2; } wishlist-button.position-absolute, wishlist-button-pdp.position-absolute { position: absolute; z-index: 10; } wishlist-button.top-left, wishlist-button-pdp.top-left { top: 0; left: 0; } wishlist-button.top-right, wishlist-button-pdp.top-right { top: 0; right: 0; } wishlist-button.bottom-left, wishlist-button-pdp.bottom-left { bottom: 0; left: 0; } wishlist-button.bottom-right, wishlist-button.bottom-right-pdp { bottom: 0; right: 0; } wishlist-button[data-is-added="true"] .added-state, wishlist-button-pdp[data-is-added="true"] .added-state, wishlist-button-social-proof[data-is-added="true"] .added-state { display: flex !important; } wishlist-button[data-is-added="true"] .empty-state, wishlist-button-pdp[data-is-added="true"] .empty-state, wishlist-button-social-proof[data-is-added="true"] .empty-state { display: none; } .atw-drawer__close, .wishlists-drawer__close { display: inline-block; padding: 0; min-width: 34px; min-height: 34px; box-shadow: 0 0 0 .2rem rgba(var(--color-button), 0); position: absolute; top: 10px; right: -10px; color: rgb(var(--color-foreground)); background-color: transparent; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; } .atw-drawer__close svg { height: 13px; width: 13px; } .atw-drawer__heading, .wishlists-drawer__heading { margin: 0 0 0; font-size: 28px; padding: 0; } .atw-drawer__header, .wishlists-drawer__header { position: relative; padding: 15px 0 5px; display: flex; align-items: center; } .customer-login-banner { line-height: 1.5; text-align: center; padding: 10px; } .customer-login-banner p { margin: 0; } .customer-login-banner a { text-decoration: underline; } .customer-login-banner a:hover, .atw-drawer__item-cta open-wishlists-button:hover { opacity: 0.75; } .wishlist-card:not(.wishlist-card--wishlist) { /* margin-top: 30px; */ } .wishlist-card:not(.wishlist-card--wishlist) .button--cta-remove, .wishlist-card:not(.wishlist-card--wishlist) .wishlist-drawer__delete-button { display: none !important; } .wishlists-drawer--single-wishlist wishlist-element { padding: 0; border: none; box-shadow: none; max-height: 100%; } .wishlists-drawer__wishlists-container.with-email-consent { max-height: calc(100% - 150px); overflow: auto; } .wishlist-drawer__wishlist-items-cards-container { margin-bottom: 10px; } .wishlists-drawer--single-wishlist .expanded .wishlist-card--wishlist .wishlist-drawer__wishlist-items-cards-container { max-height: calc(100vh - 90px); } .wishlists-drawer--single-wishlist .wishlist-card--wishlist .wishlist-drawer__item-header { display: none; } .wishlists-drawer--single-wishlist .wishlist-card--wishlist .wishlist-drawer__bottom-row { display: none } .wishlists-drawer--single-wishlist .wishlist-card--wishlist .wishlist-drawer__wishlist-items-cards-container { max-height: calc(100vh - 180px); } .marketing-form .fish-field-row { display: flex; align-items: center; justify-content: left; } form.marketing-form { display: flex; align-items: center; justify-content: left; gap: 10px; margin-top: 20px; } .marketing-form input[type="submit"].button--link { background: transparent; border: none; text-decoration: underline; color: inherit; cursor: pointer; font-size: 14px; margin-left: auto; } .marketing-form .success-message { padding: 0; margin: 0; } .marketing-form .success-message.is-hidden { display: none; } .marketing-form .success-message:not(.is-hidden) ~ *{ opacity: 0; visibility: hidden; display: none; } .marketing-form label { cursor: pointer; line-height: 1.25; } wishlist-button > * { min-width: 32px; min-height: 32px; display: flex; align-items: center; justify-content: center; } marketing-opt-in form { display: flex; flex-direction: column; gap: 8px; } marketing-opt-in.marketing-form button { margin: 0 auto; margin-top: 10px; padding: 10px 20px; } marketing-opt-in { margin: auto 0 0; } button[is="open-wishlists-button"], open-wishlists-button { background: none; border: none; display: flex; align-items: center; justify-content: center; padding: 0 10px; cursor: pointer; position: relative; } [data-block-handle="open-wishlists-button"].shopify-app-block { display: flex; align-items: center; justify-content: center; position: relative; overflow: visible; } [data-block-handle="open-wishlists-button"] open-wishlists-button{ position: relative; } [data-block-handle="open-wishlists-button"] open-wishlists-button, [data-block-handle="open-wishlists-button"] open-wishlists-button svg { /* max-width: 42px; */ max-height: 42px; } wishlist-button svg { max-width: 36px; max-height: 36px; } wishlist-button-pdp svg { max-width: 32px; max-height: 32px; } .wishlist-card { padding: 8px; } .wishlist-variant-selector__images img, wishlist-item__placeholder img, .item-card__image img, wishlist-toast .fish-toast__product-image img { padding: 0; border: 1px solid rgb(16 16 16 / 7%); border-radius: 5px; box-shadow: rgba(26, 26, 26, 0.07) 0px 3px 1px -1px; } a.button--link.link--text.button--cta-remove:hover { color: currentColor; } a.fish-wishlist-button--link, open-wishlists-button.fish-wishlist-button--link { font-size: 14px; letter-spacing: 0; text-decoration: underline; cursor: pointer !important; } wishlist-button-pdp { cursor: pointer; } wishlist-button-pdp { display: flex; align-items: center; justify-content: center; } wishlist-button-pdp span { display: flex; align-items: center; justify-content: center; gap: 5px; } .wishlist-count-bubble { position: absolute; background-color: currentColor; height: 17px; width: 17px; border-radius: 100%; display: flex; justify-content: center; align-items: center; font-size: 9px; left: 22px; line-height: 10px; transform: translateY(6px); } .wishlist-count-bubble span{ color: white; } .wishlist-count-bubble:empty { display: none; } wishlist-button-social-proof .empty-state, wishlist-button .empty-state, wishlist-button-pdp .empty-state{ display: flex; height: auto; margin: auto; padding: 0; gap: 5px; } request-quote-button, buy-all-button { text-align: center; } request-quote-button.submitted, request-quote-button.submitting, buy-all-button.submitted, buy-all-button.submitting { pointer-events: none; } .fish-wishlist-button { position: relative; transition: all .3s; cursor: pointer; } .fish-wishlist-button.submitting span{ opacity: 0; } a.fish-wishlist--url { padding: 10px; font-size: 12px; background: #d3d3d35e; border-radius: 3px; margin-top: 4px; margin-bottom: 4px; color: black; } .header__icons .shopify-app-block { display: flex; position: relative; overflow: visible; } quote-quantity-picker button { border: none; background: none; cursor: pointer; } quote-quantity-picker .quote-quantity-picker__current { font-size: 14px; } quote-quantity-picker { border: 1px solid rgb(16 16 16 / 7%); margin: 0; display: flex; align-items: center; justify-content: center; width: auto; gap: 5px; border-radius: 4px; max-width: 60px; margin: 5px 0; } request-quote-button, share-wishlist-button, buy-all-button { margin-bottom: 8px; } .fish-wishlist-button.submitting::before { opacity: 1; content: ""; display: block; width: 18px; height: 18px; flex: 0 0 18px; box-shadow: -1px 0 0 currentColor; animation: spin 1s infinite linear; border-radius: 100%; margin: 0; transform-origin: center; z-index: 2; position: absolute; top: calc(50% - 9px); left: calc(50% - 9px); } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } fish-wishlist-share-button { cursor: pointer; display: flex; align-items: center; gap: 10px; } fish-wishlist-share-button > * { flex: 1; } button.fish-wishlist-share-button { display: flex; align-items: center; justify-content: center; border: none; cursor: pointer; margin-top: 10px; margin-bottom: 10px; padding: 10px; } button.fish-wishlist-share-button svg { min-width: 20px; max-width: 20px; max-height: 12px; } .item-card__details .product-option { font-size: 1.4rem; word-break: break-word; line-height: calc(1 + 0.5 / var(--font-body-scale)); } .item-card__details .product-option * { display: inline; margin: 0; font-size: 13px; } .item-card__details .product-option+.product-option { margin-top: 0; } [data-block-handle="atw-button-pdp"] { width: 100%; } .fish-toast__content { display: flex; flex-direction: row; align-items: center; gap: 10px; } .fish-toast__product-image img { max-width: 60px; } p.fish-toast__message { margin: 0; line-height: 1.25; font-size: 14px; } .fish-toast__product-image { display: flex; } /* Fly to cart animation */ fly-to-cart { position: fixed; width: 40px; height: 40px; left: 0; top: 0; border-radius: 50%; z-index: calc(infinity); pointer-events: none; opacity: 0; overflow: hidden; box-shadow: 0 4px 8px rgb(0 0 0 / 20%); transition: opacity 0.3s ease; background-position: center center; background-size: cover; background-repeat: no-repeat; background-color: var(--color-foreground); transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1)); } :root { --duration-default: .4s } .wishlists-drawer { position: fixed; z-index: 10001; left: 0; top: 0; width: 100vw; height: 100%; display: flex; justify-content: flex-end; background-color: #12121280; transition: visibility var(--duration-default) ease; visibility: hidden; } :root { --wishlist-toast-width: 260px; } wishlist-toast { position: fixed; width: var(--wishlist-toast-width); height: auto; background: var(--color-background, white); padding: 10px; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); z-index: 10002; transform: translate(100%); transition: transform .4s ease; transition-delay: .4s; border: .1rem solid rgba(var(--color-foreground), .2); border-radius: 2px; } wishlist-toast .fish-product-title { font-weight: bold; } wishlist-toast.fish-position-top-right { top: 40px; right: -5px; } wishlist-toast.fish-position-top-left { top: 40px; right: calc(100% + var(--wishlist-toast-width)); } wishlist-toast.fish-position-bottom-right { bottom: 40px; right: -5px; } wishlist-toast.fish-position-bottom-left { bottom: 40px; right: calc(100% + var(--wishlist-toast-width)); } body.gradient wishlist-toast { background: var(--gradient-background, white); } wishlist-toast.active { display: block; } wishlist-toast.fish-position-top-left.active, wishlist-toast.fish-position-bottom-left.active { transform: translate(calc(200% + 20px), 0); } wishlist-toast.fish-position-top-right.active, wishlist-toast.fish-position-bottom-right.active { transform: translate(-20px, 0); } .wishlists-drawer.active { visibility: visible; } .wishlists-drawer__overlay:empty { display: block; } wishlists-drawer[data-open="false"] img { display: none !important; } .wishlists-drawer__overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .wishlists-drawer.active .wishlists-drawer__inner { transform: translate(0); overflow: auto; } .wishlists-drawer__inner { overflow: auto; height: 100%; width: 450px; max-width: calc(100vw - 30px); padding: 0 15px 15px; border: .1rem solid rgba(var(--color-foreground), .2); border-right: 0; overflow: hidden; display: flex; flex-direction: column; transform: translate(100%); transition: transform .4s ease; /* background: white; */ background: var(--color-background, white); } body.gradient .wishlists-drawer__inner { background: var(--gradient-background, white); } .wishlists-drawer__close svg { height: 13px; width: 13px; } .wishlist-drawer__item-header { display: flex; align-items: start; justify-content: space-between; } .wishlist-drawer__item-name { display: flex; flex-direction: column; gap: 0; } span.wishlist-title { margin-bottom: 0; margin-top: 0; font-size: 24px; } .wishlist-drawer__item-name p.items-count { margin: 0; } .wishlists-drawer__wishlists-container { display: flex; flex-direction: column; gap: 0; margin-top: 0; } wishlist-element { margin: 8px 0; display: flex; flex-direction: column; } wishlist-item-card wishlist-button { display: none; } .wishlist-items-cards-container--grid wishlist-item-card a.button--cta-remove { display: none !important; } .wishlist-items-cards-container--grid wishlist-item-card wishlist-button { display: flex; } .wishlist-items-cards-container--grid .item-card__image { aspect-ratio: 1; } .wishlist-items-cards-container--grid .item-card__image img { max-width: 100%; width: 100%; height: 100%; object-fit: contain; aspect-ratio: 1; } .wishlist-items-cards-container--grid { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 10px; } .wishlist-items-cards-container--grid .wishlist-button { display: block; } .wishlist-items-cards-container--grid p.wishlist-item-card__empty { grid-column: span 2; } .wishlist-drawer__wishlist-items-cards-container.empty ~ * { display: none; } .wishlist-items-cards-container--vertical-list .wishlist-drawer__wishlist-item-card { display: grid; grid-template-columns: 80px 1fr auto; align-items: center; } .wishlist-items-cards-container--vertical-list .item-card__image { aspect-ratio: 1; } .wishlist-items-cards-container--vertical-list .item-card__image img { max-width: 80px; height: 100%; width: 100%; object-fit: contain; } .item-card__details .select__select { height: 30px; padding: 0 8px; appearance: none; border: none; width: calc(100% - 25px); border-radius: 4px; } .item-card__details .fish-select svg { max-width: 10px; margin-right: 8px; } .wishlist-items-cards-container--grid .item-card__atc-container button { width: 100%; } .wishlist-items-cards-container--grid .item-card__details { padding-left: 0; padding-right: 0; padding-bottom: 15px; } .wishlist-items-cards-container--grid .wishlist-drawer__wishlist-item-card { height: 100%; display: flex; flex-direction: column; } .item-card__details h4 { font-size: 16px; line-height: 1.25; margin: 0; } .item-card__details h4 a { text-decoration: none; color: inherit; } .item-card__atc-container button { padding: 10px 10px; min-width: auto; min-height: auto; font-size: 13px; } .item-card__atc-container a.button--cta-remove { font-size: 12px; padding: 0; } wishlist-element .item-card__atc-container a.button--cta-remove { text-decoration: underline; cursor: pointer; } .item-card__atc-container { display: flex; align-items: center; justify-content: center; padding: 0 5px 0; flex-direction: column; gap: .5rem; text-align: center; } .wishlist-items-cards-container--vertical-list .item-card__atc-container { max-width: 160px; } .wishlist-items-cards-container--grid .item-card__atc-container { margin-top: auto; padding: 0; } .item-card__details .price-container { margin: 0; font-size: 14px; line-height: 1.25; } .price-container .was-price { text-decoration: line-through; } .wishlist-items-cards-container--vertical-list { display: flex; flex-direction: column; gap: 1rem; } .wishlist-items-cards-container--vertical-list wishlist-item-card:first-child { margin-top: 1rem; } .item-card__image { display: flex; align-items: center; justify-content: center; position: relative; } .item-card__details { padding: 10px; } .expanded .wishlist-drawer__wishlist-items-cards-container { max-height: 430px; padding: 0 0 3px; } .wishlist-drawer__wishlist-items-cards-container { max-height: 0; overflow: auto; transition: max-height .3s ease; } .expanded .wishlist-item__placeholders-container { opacity: 0; } .wishlist-item__placeholders-container { opacity: 1; transition: opacity .3s ease; } .wishlist-drawer__item-cta .hide-copy { display: none; } .wishlist-drawer__item-cta .show-copy { display: flex; } .expanded .wishlist-drawer__item-cta .hide-copy { display: flex; } .expanded .wishlist-drawer__item-cta .show-copy { display: none; } .wishlist-drawer__item-cta a svg path { fill: currentColor; } .atc-error { font-size: 12px; line-height: 1.5; color: darkred; } .wishlist-drawer__bottom-row { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; } .wishlist-drawer__delete-button path { stroke-width: 1.4px; fill: none; color: #dd1d1d; stroke: currentColor; } .wishlist-drawer__delete-button svg { width: 100%; inline-size: 100%; width: 16px; height: 16px; cursor: pointer; } button.wishlist-drawer__delete-button { border: none; padding: 0; background: none; } wishlists-drawer:not(.empty) .wishlist-drawer__empty-wishlist { display: none; } .wishlists-drawer.empty .customer-login-banner { display: none; } p.item-card__variant-title { margin: 0; line-height: 1.25; font-size: 14px; } p.item-card__variant-title:empty { display: none; } p.item-card__metafield { margin: 0; line-height: 1.25; font-size: 13px; } button.wishlists-drawer__back.is-hidden { display: none; } button.wishlists-drawer__back { background: transparent; border: none; padding: 0; display: flex; margin-right: 5px; border: 1px solid currentColor; color:currentColor; border-radius: 20px; cursor: pointer; padding: 1px; } button.wishlists-drawer__back svg { width: 16px; } button.wishlists-drawer__back svg path { fill: currentColor; } .wishlist-drawer__item-cta .show-copy, .wishlist-drawer__item-cta .hide-copy { align-items: center; } .wishlist-drawer__item-cta .show-copy svg, .wishlist-drawer__item-cta .hide-copy svg { width: 20px; } .item-card__image img, .price-container p { transition: all .3s; } .item-card__image img.is-hidden, .price-container p.is-hidden { opacity: 0; visibility: hidden; position: absolute; z-index: -1; } .price-container p.is-hidden { display: none; } .item-card__image img { position: absolute; } .item-card__image img:not(.is-hidden), .price-container p:not(.is-hidden) { opacity: 1; /* visibility: visible; */ display: block; } .wishlist-drawer__wishlist-item-card .price-container p { margin: 0; } .item-card__details .fish-select { margin: 5px 0; border: 1px solid rgb(16 16 16 / 7%); display: flex; } .wishlist-items-cards-container--vertical-list .wishlist-drawer__wishlist-item-card .item-card__details { height: 100%; padding: 0 10px; } .item-card__details .fish-select select:focus, .item-card__details .fish-select select:focus-visible { border: none !important; box-shadow: none !important; outline: none !important; } request-quote-button[disabled="disabled"], buy-all-button[disabled="disabled"] { pointer-events: none; } .fish-textarea.quote-note { margin: 12px 0; padding: 6px; font-size: 14px; } .atw-drawer { position: fixed; z-index: 10000; left: 0; top: 0; width: 100vw; height: 100%; display: flex; justify-content: flex-end; background-color: #12121280; transition: visibility .4s ease; } .atw-drawer { visibility: hidden; } .atw-drawer.active { visibility: visible; } .atw-drawer__overlay:empty { display: block; } .atw-drawer__overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .atw-drawer.active .atw-drawer__inner { transform: translate(0); overflow: scroll; } .atw-drawer__inner { overflow: scroll; height: 100%; width: 450px; max-width: calc(100vw - 30px); padding: 0 15px 15px; border-right: 0; display: flex; flex-direction: column; transform: translate(100%); transition: transform var(--duration-default) ease; /* background: white; */ background: var(--color-background, white); } body.gradient .atw-drawer__inner { background: var(--gradient-background, white); } .atw-drawer__inner .field { display: flex; flex-direction: column-reverse; } .atw-drawer__inner .field label { font-size: 14px; } .atw-drawer__inner input[name="wishlistName"] { padding: 6px 18px; background: transparent; } atw-drawer-item { display: flex; flex-direction: column; } .atw-drawer__item-in-list { display: none; line-height: 1.5; align-items: center; justify-content: center; font-size: 12px; margin-left: 8px; color: darkgreen; } .atw-drawer__item-in-list:before { content: ""; width: 5px; height: 5px; background: currentColor; border-radius: 50px; margin-right: 5px; } atw-drawer-item[data-is-added="true"] .atw-drawer__item-in-list{ display: flex; } p.items-count { /* opacity: 0.5; */ font-size: 14px; line-height: 1.5; } .atw-drawer__item-header { display: flex; align-items: start; justify-content: space-between; } .atw-drawer__item-name { display: flex; flex-direction: column; } .atw-drawer atw-drawer-item .button:disabled { opacity: 0.5; display: none; } .atw-drawer__item-name .wishlist-title { margin: 0; font-size: 22px; } .atw-drawer__item-name p { margin: 0; } .wishlist-item__placeholder { min-width: 30px; height: 30px; border: 1px solid lightgrey; border-radius: 3px; display: flex; align-items: center; justify-content: center; letter-spacing: 0; font-size: 14px; } .wishlist-item__placeholders-container:empty { display: none; } .wishlist-item__placeholders-container { display: flex; gap: 4px; } .atw-drawer-items__container { display: flex; flex-direction: column; gap: 12px; } new-wishlist-form { display: flex; flex-direction: column; gap: 15px; } new-wishlist-form .field__input { border: 1px solid lightgrey } new-wishlist-form input[type="submit"] { width: 100%; margin-top: 10px; } new-wishlist-form#new-wishlist-form label { display: block; } new-wishlist-form#new-wishlist-form input { width: 100%; } new-wishlist-form#new-wishlist-form form { margin: 0; } new-wishlist-form#new-wishlist-form .fish-field-row { width: 100%; } atw-drawer-item .button--cta-remove { display: none; cursor: pointer; } atw-drawer-item .button--cta:disabled ~ .button--cta-remove { display: block; } .button--cta:disabled ~ a.button--cta-remove { display: block; font-size: 12px; } .atw-drawer__item-cta { display: flex; flex-direction: column; align-items: center; gap: 8px; } [data-variant-selected="false"] + .atw-drawer__wishlists-container, [data-variant-selected="false"] + add-to-wishlist-form { opacity: 0; visibility: hidden; } add-to-wishlist-form { transition: all .3s; } add-to-wishlist-form button.fish-wishlist-button { display: flex; align-items: center; justify-content: center; width: 100%; } add-to-wishlist-form .button--link.button--cta-remove { text-align: center; } .atw-drawer__wishlists-container { transition: all .3s; } .atw-drawer__body-header-variant-selector { margin-bottom: 5px; font-size: 18px; } add-to-wishlist-form .button--cta:disabled { display: none; } wishlist-variant-selector .fish-select select { box-shadow: none !important; border: none !important; width: 100%; appearance: none; -webkit-appearance: none; display: flex; align-items: center; } wishlist-variant-selector .fish-select select:focus, wishlist-variant-selector .fish-select select:focus-visible { border: none !important; outline: none !important; } .wishlist-variant-selector__images img.is-hidden { display: none !important; } .wishlist-variant-selector__titles h4.is-hidden { display: none !important; } .atw-drawer .atw-drawer__body-header-variant-selector, .atw-drawer wishlist-variant-selector { display: none; } .atw-drawer__body-header { margin-bottom: 0; } .atw-drawer.show-variant-selector .atw-drawer__body-header-variant-selector, .atw-drawer.show-variant-selector wishlist-variant-selector { display: flex; flex-direction: column; } .atw-drawer.show-variant-selector wishlist-variant-selector .wishlist-variant-selector__card { display: grid } wishlist-variant-selector .wishlist-variant-selector__card { grid-template-columns: 100px auto; grid-column-gap: 10px; } wishlist-variant-selector { width: 100%; } .wishlist-variant-selector__images { display: flex; align-items: center; justify-content: center; } .wishlist-variant-selector__images img { max-width: 100px; } .wishlist-variant-selector__titles h4 { margin: 0; margin-top: 10px; margin-bottom: 10px; font-size: 16px; } .wishlist-item__placeholder img { max-height: 28px; width: auto; } atw-drawer.empty .atw-drawer__body-header--existing, atw-drawer.empty .atw-drawer__divider, atw-drawer.empty .customer-login-banner { display: none; } atw-drawer .select__select { height: 35px; padding:0; margin: 0; background: transparent; } wishlist-variant-selector { margin-bottom: 12px; } wishlist-variant-selector .fish-select { padding: 0 12px; border: 1px solid lightgrey; position: relative; } .customer-login-banner { margin: 12px 0; } .customer-login-banner a { color: inherit; } input[name="wishlistName"]::placeholder { opacity: 1; } .atw-drawer__divider { display: inline-block; margin: 15px 0; width: 100%; position: relative; display: inline-block; background: inherit; } .atw-drawer__divider:after { content: ""; border-top: 1px solid lightgrey; height: 1px; width: 100%; position: absolute; left: 0; top: 0; right: 0; bottom: 0; } .atw-drawer__divider span { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); background: inherit; z-index: 2; color: inherit; background: var(--gradient-background,#fff); padding: 0 1rem; white-space: nowrap; } .atw-drawer__body-header--new { margin-top: 1.5rem; margin-bottom: .5rem; } .wishlist-variant-selector__content .fish-select svg { max-width: 10px; margin: 0 10px; position: absolute; right: 0; top: 50%; transform: translateY(-50%); } .item-card__atc-container a.button--cta-remove { display: block; } .atw-drawer.full .atw-drawer__divider, .atw-drawer.full .atw-drawer__body-header, .atw-drawer.full new-wishlist-form { display: none; } .button--cta:disabled .add-to-list { display: none; } .button--cta:not(:disabled) .item-in-list { display: none; } #new-wishlist-form .error-message:empty { display: none; } .error-message { color: darkred; font-size: 14px; } p.wishlist-item-card__empty { margin-bottom: 0; text-align: center; } @media screen and (max-width: 768px) { atw-drawer-item { margin: 10px 0; } } add-to-wishlist-form button { width: 100%; } .wishlist-variant-selector__titles .fish-select { border: 1px solid currentColor; } .atw-drawer__item-cta a[role="button"] { cursor: pointer; font-size: 14px; text-decoration: underline; } .wishlist-variant-selector__content .fish-select:before, .wishlist-variant-selector__content .fish-select:after{ content: none; } .atw-drawer__item-name open-wishlists-button { text-align: left; display: flex; justify-content: flex-start; padding: 0; } .wishlist-item__row { display: flex; align-items: center; justify-content: flex-start; margin-top: 1rem; } .wishlist-item__placeholders-container { margin-right: 5px; } .atw-drawer__item-cta open-wishlists-button { width: 100%; } .atw-drawer__item-cta open-wishlists-button svg { max-width: 20px; } .atw-drawer__item-cta open-wishlists-button svg path, add-to-wishlist-form open-wishlists-button svg path { fill: currentColor; color: currentColor; stroke: none; } .atw-drawer__item-cta open-wishlists-button, add-to-wishlist-form open-wishlists-button { color: inherit; white-space: nowrap; } add-to-wishlist-form#add-to-wishlist-form .button--cta:not(:disabled) ~ .button--cta-remove { display: none !important; } add-to-wishlist-form open-wishlists-button svg { max-width: 20px; } add-to-wishlist-form open-wishlists-button { margin-top: 10px; display: flex; align-items: center; justify-content: center; gap: 2px; } open-wishlists-button { background: none; border: none; /* padding: 0; */ display: flex; align-items: center; justify-content: center; cursor: pointer; } wishlists-social-proof { display: flex; align-items: center; justify-content: space-between; } wishlists-social-proof:not(.loaded) { display: none; } wishlist-button-social-proof { cursor: pointer; background-color: white; border-radius: 45px; border: 1px solid transparent; padding: 4px 12px; display: inline-block; } wishlist-button-social-proof > * { gap: 5px; display: flex; align-items: center; justify-content: center; line-height: 1; } wishlist-button-social-proof > span { display: flex; align-items: center; justify-content: center; line-height: 1; gap: 5px; font-size: 14px; } .wishlist-social-proof__column p, .wishlist-social-proof__column h5 { margin: 0; font-size: 14px; } .wishlist-social-proof__column p.is-hidden { display: none; } open-wishlists-button svg { max-width: max-content; } .card-hover-effect-lift .wishlist-count-bubble span { color: currentColor; filter: invert(1); } .card-hover-effect-lift .wishlist-count-bubble { top: 0px; left: 25px; height: 20px; width: 20px; } .card-hover-effect-lift open-wishlists-button svg { stroke-width: 1.75px; transform: scale(.85); } .wishlists-drawer share-wishlist-button, .wishlists-drawer request-quote-button, .wishlists-drawer buy-all-button { width: 100%; } /* Row layout when both Request Quote and Buy All buttons are enabled */ .wishlist-drawer__action-buttons-row { display: flex; gap: 8px; width: 100%; margin-bottom: 8px; } .wishlist-drawer__action-buttons-row request-quote-button, .wishlist-drawer__action-buttons-row buy-all-button { flex: 1; width: 50%; margin-bottom: 0; } [data-block-handle="wishlist-page"] { width: 100%; } new-wishlist-form input[type="submit"] { background: var(--button-background-color, black); } button.fish-wishlist-button { padding: 10px 10px; }
// Shopify money formatting utility Shopify.formatMoney = function(cents, format) { if (typeof cents === 'string') { cents = cents.replace('.', ''); } const placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; const formatString = format || this.money_format; function formatWithDelimiters(number, precision = 2, thousands = ',', decimal = '.') { if (isNaN(number) || number == null) { return 0; } number = (number / 100.0).toFixed(precision); const parts = number.split('.'); const dollars = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands); const cents = parts[1] ? (decimal + parts[1]) : ''; return dollars + cents; } const match = formatString.match(placeholderRegex); if (!match) return formatString; let value = ''; switch (match[1]) { case 'amount': value = formatWithDelimiters(cents, 2); break; case 'amount_no_decimals': value = formatWithDelimiters(cents, 0); break; case 'amount_with_comma_separator': value = formatWithDelimiters(cents, 2, '.', ','); break; case 'amount_no_decimals_with_comma_separator': value = formatWithDelimiters(cents, 0, '.', ','); break; } return formatString.replace(placeholderRegex, value); }; function formatId(id) { if (!id) return ""; return id.toString().split("/").pop(); }
class StorefrontApiClient { constructor({token, variants, country, lang, proxyUrl}) { this.apiVersion = "2025-07"; this.token = token; this.variants = variants; this.country = country; this.lang = lang; this.proxyUrl = proxyUrl; } getValidMetafields(metafieldDisplays = []) { return metafieldDisplays.filter((mf) => mf?.namespace && mf?.key); } async query(query, variables) { const resp = await fetch(`/api/${this.apiVersion}/graphql.json`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Shopify-Storefront-Access-Token': this.token }, body: JSON.stringify({ query, variables }) }); const json = await resp.json(); if (json.errors) console.error('[FishWishlist] Storefront API errors:', JSON.stringify(json.errors)); if (!json.data) console.error('[FishWishlist] No data in response. Full response:', JSON.stringify(json)); return json.data; } async fetchStorefrontVariants(variantIds, metafieldDisplays = []) { const validMetafields = this.getValidMetafields(metafieldDisplays); const variantsQuery = this.variants ? `variants(first: 100) { nodes { id title } }` : ''; // Build dynamic metafield queries from configured namespace:key pairs const metafieldQuery = validMetafields .map((mf, i) => `mf_${i}: metafield(namespace: "${mf.namespace}", key: "${mf.key}") { value type key namespace reference { ... on Metaobject { title: field(key: "title") { value } } } }`) .join('\n '); const query = ` query ProductVariants($ids: [ID!]!) @inContext(country: ${this.country}, language: ${this.lang}) { nodes(ids: $ids) { ... on ProductVariant { id title availableForSale sku compareAtPrice { amount } price { amount } featured_image: image { src: url(transform: {maxWidth: 350, maxHeight: 350}) src_small: url(transform: {maxWidth: 120, maxHeight: 120}) src_large: url(transform: {maxWidth: 500, maxHeight: 500}) } product { id title onlineStoreUrl handle tags featuredImage { url(transform: {maxWidth: 350, maxHeight: 350}) } ${variantsQuery} ${metafieldQuery} } } } } `; const variables = { ids: variantIds.map(v => `gid://shopify/ProductVariant/${v}`) }; return await this.query(query, variables); } async fetchB2BVariants(variantIds, customer, metafieldDisplays = []) { const params = new URLSearchParams({ ids: variantIds.join(","), company_location_id: customer?.currentLocation?.id || "", country_code: this.country, get_variants: this.variants }); // Pass metafield config so B2B endpoint can include them in the query const validMetafields = this.getValidMetafields(metafieldDisplays); if (validMetafields.length) { params.set("metafields", JSON.stringify(validMetafields)); } const url = `${this.proxyUrl}/b2b/variants?${params.toString()}`; const b2bResponse = await fetch(url); const b2bJson = await b2bResponse.json(); return b2bJson; } async fetchOtherVariants(variantIds, customer = null, metafieldDisplays = []) { if (customer?.currentLocation?.id) return await this.fetchB2BVariants(variantIds, customer, metafieldDisplays); return await this.fetchStorefrontVariants(variantIds, metafieldDisplays); } }
class MetafieldDataConnector { constructor({proxyUrl, metadata, wishlists, abandonedCartData, previouslyPurchasedData, lang, wishlistsKey, metadataKey}) { this.proxyUrl = proxyUrl; this.metadata = metadata; this.wishlists = wishlists; this.abandonedCartData = abandonedCartData; this.previouslyPurchasedData = previouslyPurchasedData; this.wishlistsKey = wishlistsKey; this.metadataKey = metadataKey; this.lang = lang?.toLowerCase(); } setWishlists(metadata, wishlists, variantId, wishlistId, action, wishlistName) { fetch(`${this.proxyUrl}/wishlist/set`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ metadata, wishlist: wishlists[wishlistId]?.map(v => v.id) || [], variantId, wishlistId, action, wishlistName }) }).catch(err => console.error("[FishWishlist] setWishlists error:", err)); } setMetadata(metadata) { return fetch(`${this.proxyUrl}/wishlist/metadata/set`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ metadata }) }); } addToWishlist(metadata, wishlists, variantId, wishlistId, productId) { this.setWishlists(metadata, wishlists, variantId, wishlistId, "ADD", null); } removeFromWishlist(metadata, wishlists, variantId, wishlistId) { this.setWishlists(metadata, wishlists, variantId, wishlistId, "REMOVE"); } addWishlist(metadata, wishlists, wishlistId, wishlistName) { this.setWishlists(metadata, wishlists, null, wishlistId, "WISHLIST_ADD", wishlistName); } removeWishlist(metadata, wishlists, wishlistId, wishlistName) { this.setWishlists(metadata, wishlists, null, wishlistId, "WISHLIST_REMOVE", wishlistName); } bulkAddWishlists(metadata, wishlists, customerHadWishlists) { fetch(`${this.proxyUrl}/wishlist/bulk_add`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ metadata, wishlists, customerHadWishlists }) }).catch(err => console.error("[FishWishlist] bulkAddWishlists error:", err)); } mergeWishlist(metadata, wishlistToMerge, customerHadWishlists) { fetch(`${this.proxyUrl}/wishlist/merge`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ metadata, wishlistToMerge, customerHadWishlists }) }).catch(err => console.error("[FishWishlist] mergeWishlist error:", err)); } getMetadata() { return this.metadata; } getWishlists(metadata) { // Return all wishlists that have items, regardless of metadata presence. // Missing metadata entries are reconciled in FishWishlistApp.syncMissingMetadata(). return { ...this.wishlists }; } loadCustomSection(metadata, wishlists, type, sectionSettings) { let id; let data = null; switch (type) { case "ABANDONED_CART": id = "WL11"; data = this.abandonedCartData; break; case "PREVIOUSLY_PURCHASED": id = "WL12"; data = this.previouslyPurchasedData; break; } if (!id || !data) return; wishlists[id] = data; const metaElement = { id: id, createdAt: null, name: sectionSettings?.title?.[this.lang], type: type }; const currentMetaIndex = metadata.findIndex(m => m.id === id); if (currentMetaIndex > -1) metadata[currentMetaIndex] = metaElement; else metadata.push(metaElement); } }
class LocalStorageDataConnector { constructor({maxWishlists, metadataKey, wishlistsKey, wishlistsIds}) { this.metadataKey = metadataKey; this.wishlistsKey = wishlistsKey; this.maxWishlists = maxWishlists; this.wishlistsIds = wishlistsIds; } addToWishlist(metadata, wishlists) { localStorage.setItem(this.wishlistsKey, JSON.stringify(wishlists)); } removeFromWishlist(metadata, wishlists) { localStorage.setItem(this.wishlistsKey, JSON.stringify(wishlists)); } addWishlist(metadata, wishlists) { localStorage.setItem(this.wishlistsKey, JSON.stringify(wishlists)); localStorage.setItem(this.metadataKey, JSON.stringify(metadata)); } removeWishlist(metadata, wishlists) { localStorage.setItem(this.wishlistsKey, JSON.stringify(wishlists)); localStorage.setItem(this.metadataKey, JSON.stringify(metadata)); } setMetadata(metadata) { localStorage.setItem(this.metadataKey, JSON.stringify(metadata)); } setWishlists(metadata, wishlists) { localStorage.setItem(this.wishlistsKey, JSON.stringify(wishlists)); } // No-op for local storage — bulk operations are handled by the metafield connector bulkAddWishlists(metadata, wishlists) {} parseStorageValue(key, fallback) { const rawValue = localStorage.getItem(key); if (!rawValue || rawValue === "null" || rawValue === "undefined") return fallback; try { return JSON.parse(rawValue); } catch (error) { console.error(`[FishWishlist] Invalid JSON in localStorage key "${key}"`, error); return fallback; } } getMetadata() { const storedMetadata = this.parseStorageValue(this.metadataKey, []); const hasInvalidMarker = Array.isArray(storedMetadata) && storedMetadata.some((w) => w?.id === "currVal"); const safeMetadata = Array.isArray(storedMetadata) && !hasInvalidMarker ? storedMetadata : []; if (hasInvalidMarker) localStorage.setItem(this.metadataKey, "[]"); return safeMetadata.slice(0, this.maxWishlists).map((w) => ({ ...w, type: "WISHLIST" })); } getWishlists(metadata) { const emptyObj = this.wishlistsIds.reduce((acc, currVal) => { acc[currVal] = []; return acc; }, {}); let wishlistsData = this.parseStorageValue(this.wishlistsKey, emptyObj); if (wishlistsData?.currVal) { localStorage.removeItem(this.metadataKey); localStorage.removeItem(this.wishlistsKey); wishlistsData = emptyObj; } const parsed = Object.keys(wishlistsData).reduce((acc, currVal) => { if (currVal === "currVal") return acc; const items = Array.isArray(wishlistsData[currVal]) ? wishlistsData[currVal] : []; acc[currVal] = items.filter((item) => item?.id); return acc; }, {}); return parsed; } // No-op for local storage — custom sections are handled by the metafield connector loadCustomSection(metadata, wishlists, type, sectionSettings) {} }
class ThemeManager { constructor(app) { this.app = app this.current = Shopify.theme this.setElements() this.setFunctions() } setElements() { this.elements = { cartBubble: document.querySelectorAll(this.getCartBubbleSelector()) } } setFunctions() { this.functions = { cartUpdate: this.getCartUpdateFunction() } } getCartBubbleSelector() { switch (this.current?.schema_name) { case "Impact": switch (this.current?.schema_version) { case "6.2.0": return '.count-bubble' case "6.7.0": return '.count-bubble span:last-child' } break case "Dawn": case "Sense": return '.cart-count-bubble span:first-child' case "Distinctive": return '.thb-secondary-area .thb-item-count' case "Taiga": return '.header-item__link .cart-blip' case "Symmetry": return '.cart-link .cart-link__count' case "Xclusive": return '#cart-count' default: return null } } getCartUpdateFunction() { switch (this.current?.schema_name) { case "Ignite": case "Dawn": case "Sahara": return ({lineNo, quantity}) => { setTimeout(() => { document.querySelector('cart-drawer-items')?.updateQuantity(lineNo, quantity) document.querySelector('cart-items')?.updateQuantity(lineNo, quantity) }, 1000) } case "Heritage": case "Ritual": case "Savor": case "Atelier": case "Tinker": case "Dwell": case "Pitch": case "Vessel": case "Fabric": case "Horizon": return ({lineNo, quantity}) => { document.querySelector('cart-items-component')?.updateQuantity({line: lineNo, quantity}) setTimeout(() => { FishWishlist.helpers.insertWishlistDrawerButton(FishWishlist.settings?.selectors?.wishlistDrawerButton, FishWishlist.allWishlistItems.length) }, 1000) } case "Symmetry": return () => { document.querySelector('cart-form')?.refresh() } case "Impact": return ({quantity}) => { document.dispatchEvent(new CustomEvent("cart:refresh")) const cartCount = document.querySelector('cart-count') if (cartCount?.itemCount != null) { cartCount.itemCount += quantity document.dispatchEvent(new CustomEvent("variant:add")) } } case "Taiga": return () => {} case "Distinctive": return () => { document.dispatchEvent(new CustomEvent("cart:refresh")) } case "Process Creative": return ({quantityAdded}) => { document.dispatchEvent(new CustomEvent("onCartFinished")) setTimeout(() => { document.querySelectorAll('.c-header__link .c-header__cart-count').forEach(el => { el.innerHTML = parseInt(el.innerHTML) + quantityAdded }) }, 1000) } default: return null } } }
class FishWishlistApp { constructor({settings, dataConnector, storage, customer, apiClient, translations, currentVariantIds, currentVariantId, elements, currency, pixels}) { this.customer = customer; this.settings = settings; this.storage = storage; this.dataConnector = dataConnector; this.apiClient = apiClient; this.translations = translations; this.currentVariantIds = currentVariantIds; this.currentVariantId = currentVariantId; this.elements = elements; this.currency = currency; this.pixels = pixels; this.reservedWishlistIds = new Set(["WL11", "WL12", "WL13", "WL14", "WL15"]); this.setElements(); this.loadData(); this.moveWishlistFromStorageToMetafields(); this.setAllWishlistsItems(); this.setAdditionalData(); this.setHelpers(); this.insertElements(); this.setPixels(); this.setTheme(); this.setEvents(); this.setListeners(); } handleAppLoaded() { const params = new URLSearchParams(window.location.search); const variantIds = params.get('fvid'); const openDrawer = params.get('opendrawer'); if (variantIds) { this.apiClient.fetchStorefrontVariants(variantIds.split(',')).then(({nodes}) => { if (!nodes) return; nodes.forEach(node => { FishAPI.addToWishlist('WL1', { ...node, id: this.helpers.legacyId(node.id) }); }); this.helpers.setCount(); if (openDrawer) { setTimeout(() => { this.elements.wishlistsDrawer.open(); document.querySelector('wishlists-drawer')?.renderWishlistElements(); }, 500); } }).catch(error => { console.error('[FishWishlist] Failed to load variants from URL:', error); }); } } setListeners() { document.addEventListener(this.events.LOADED, this.handleAppLoaded.bind(this)); } setTheme() { this.theme = new ThemeManager(this); } setHelpers() { this.helpers = { insertWishlistElement: (target, element, insertPosition) => { if (!target) return; switch (insertPosition) { case 'insertafter': target.after(element); break; case 'insertbefore': target.before(element); break; case 'insertinside': target.append(element); break; } }, getElementPosition: (insertPosition) => { switch (insertPosition) { case 'topright': return 'position:absolute; top:0; right:0; left: auto; bottom: auto;'; case 'topleft': return 'position:absolute; top:0; left:0; bottom: auto; right: auto;'; case 'bottomright': return 'position:absolute; bottom:0; right:0; top: auto; left: auto;'; case 'bottomleft': return 'position:absolute; bottom:0; left:0; right: auto; top: auto;'; } }, insertWishlistDrawerButton: (settings, totalCount) => { if (!settings) return; const { cssSelector, insertPosition } = settings; if (cssSelector) { const button = document.createElement('open-wishlists-button'); button.dataset.primary = "true"; button.innerHTML = ` ${this.elements.icons.empty} <div class="wishlist-count-bubble">${totalCount ? `<span class="wishlist-count-bubble__value">${totalCount}</span>` : ''}</div> `; this.helpers.insertWishlistElement(document.querySelector(cssSelector), button, insertPosition); return button; } }, insertWishlistButton: (settings) => { if (!settings) return; const { cssSelector, insertPosition, ids } = settings; if (cssSelector) { const button = document.createElement('wishlist-button'); button.setAttribute("data-variant-id", ids.join(",")); button.innerHTML = `<span class="empty-state"> ${this.elements.icons.empty}</span> <span class="added-state" style="display:none"> ${this.elements.icons.added} </span>`; this.helpers.insertWishlistElement(document.querySelector(cssSelector), button, insertPosition); return button; } }, insertPDPButton: (settings) => { if (!settings) return; const { cssSelector, insertPosition, classes } = settings; if (cssSelector) { const button = document.createElement('wishlist-button-pdp'); button.setAttribute("data-preselected-id", this.currentVariantId); button.setAttribute("data-variant-id", this.currentVariantIds); button.setAttribute("role", "button"); if (classes?.pdpButton) button.setAttribute("class", classes.pdpButton); button.innerHTML = `<span class="empty-state"> ${this.elements.icons.empty} </span> <span class="added-state" style="display:none"> ${this.elements.icons.added} </span>`; this.helpers.insertWishlistElement(document.querySelector(cssSelector), button, insertPosition); return button; } }, insertCardButtons: (settings) => { if (!settings) return; const { cssSelector, insertPosition } = settings; if (cssSelector) { document.querySelectorAll(cssSelector).forEach(target => { const button = document.createElement('wishlist-button'); button.setAttribute("style", this.helpers.getElementPosition(insertPosition)); button.setAttribute("data-variant-id", "123456789"); button.innerHTML = `<span class="empty-state"> ${this.elements.icons.empty} </span> <span class="added-state" style="display:none"> ${this.elements.icons.added} </span>`; this.helpers.insertWishlistElement(target, button, 'insertinside'); }); } }, legacyId: (id) => { if (!id) return false; return parseInt(id.toString().split("/").pop()); }, setCount: () => { document.querySelectorAll('open-wishlists-button[data-primary]').forEach(el => el?.setCount()); } }; } setEvents() { this.events = { ITEM_ADDED: 'fishwishlist:item-added-to-wishlist', ITEM_REMOVED: 'fishwishlist:item-removed-from-wishlist', ITEM_ADDED_TO_CART: 'fishwishlist:item-added-to-cart', LOADED: 'fishwishlist:loaded' }; } setElements() { const setElement = (selector, element) => { if (!selector) return; const timeout = 10000; const interval = 100; let elapsed = 0; const elInterval = setInterval(() => { elapsed += interval; if (elapsed >= timeout) { console.error(`[FishWishlist] Could not find: ${element}. Make sure app embed is enabled.`); clearInterval(elInterval); return; } const domEl = document.querySelector(selector); if (domEl) { this.elements[element] = domEl; clearInterval(elInterval); } }, interval); }; setElement('wishlists-drawer', 'wishlistsDrawer'); setElement('atw-drawer', 'addToWishlistDrawer'); setElement('wishlist-toast', 'wishlistToast'); setElement('open-wishlists-button[data-primary]', 'openWishlistsButton'); } insertElements() { try { this.helpers.insertWishlistDrawerButton(this.settings?.selectors?.wishlistDrawerButton, this.allWishlistItems.length); this.helpers.insertPDPButton(this.settings?.selectors?.atwWishlistPDPButton); } catch (error) { console.error('[FishWishlist] Error inserting elements:', error); } } setAllWishlistsItems() { this.allWishlistItems = Object.keys(this.wishlists).reduce((acc, currVal) => { if (!this.metadata.find(m => m.id === currVal)) return acc; if (this.isReservedWishlistId(currVal)) return acc; return [...acc, ...this.wishlists[currVal]]; }, []).map(v => v.id); } loadData() { this.metadata = this.dataConnector.getMetadata(); this.wishlists = this.dataConnector.getWishlists(this.metadata); this.syncMissingMetadata(); const { abandonedCart, previouslyPurchased } = this.settings?.customSections || {}; if (abandonedCart?.enabled) this.dataConnector.loadCustomSection(this.metadata, this.wishlists, "ABANDONED_CART", abandonedCart); if (previouslyPurchased?.enabled) this.dataConnector.loadCustomSection(this.metadata, this.wishlists, "PREVIOUSLY_PURCHASED", previouslyPurchased); } // Detect wishlists that have items in metafields but are missing from metadata // (e.g. admin added items to WL3 without updating metadata). Backfill metadata // entries and persist so the wishlists display correctly. syncMissingMetadata() { const maxWishlists = parseInt(this.settings?.wishlists?.maxPerCustomer) || 3; let wishlistCount = this.metadata.filter((m) => !this.isReservedWishlistId(m.id)).length; let metadataChanged = false; for (const wId of Object.keys(this.wishlists)) { if (this.isReservedWishlistId(wId)) continue; if (!this.wishlists[wId] || this.wishlists[wId].length === 0) continue; if (this.metadata.find(m => m.id === wId)) continue; if (wishlistCount >= maxWishlists) break; this.metadata.push({ id: wId, name: this.getDefaultTitleForId(wId), createdAt: Date.now(), type: "WISHLIST" }); wishlistCount++; metadataChanged = true; } if (metadataChanged && this.isLoggedIn()) { this.dataConnector.setMetadata(this.metadata); } } moveWishlistFromStorageToMetafields() { if (!this.isLoggedIn()) return; const customerHasWishlists = this.metadata.length > 0; const storageMetadata = localStorage.getItem(this.dataConnector.metadataKey); const storageWishlists = localStorage.getItem(this.dataConnector.wishlistsKey); if (storageMetadata && storageWishlists) { const { wishlistsToAdd, wishlistToMerge } = this.mergeWishlists(JSON.parse(storageMetadata), JSON.parse(storageWishlists)); if (wishlistToMerge?.id) { this.dataConnector.mergeWishlist(this.metadata, wishlistToMerge, customerHasWishlists); } else { this.dataConnector.bulkAddWishlists(this.metadata, wishlistsToAdd, customerHasWishlists); } localStorage.removeItem(this.dataConnector.metadataKey); localStorage.removeItem(this.dataConnector.wishlistsKey); } } mergeWishlists(storageMetadata, storageWishlists) { let wishlistsToAdd = {}; let wishlistToMerge = {}; const findAvailableWishlistIds = (metadata, wishlists) => { return Object.keys(wishlists).filter(wId => !metadata.find(m => m.id === wId)); }; if (this.settings.wishlists.maxPerCustomer === '1') { const meta = this.metadata?.[0] ? this.metadata : storageMetadata; const wishlistId = meta?.[0]?.id; const existingItemsIds = (this.wishlists[wishlistId] || []).map(i => i.id.toString()); const newItems = (storageWishlists[wishlistId] || []).filter(x => !existingItemsIds.includes(x.id.toString())); const allItems = [...this.wishlists[wishlistId], ...newItems]; this.metadata = meta; this.wishlists = { ...this.wishlists, [wishlistId]: allItems }; wishlistToMerge = { id: wishlistId, newItems, allItems }; } else { for (const [i, availableId] of findAvailableWishlistIds(this.metadata, this.wishlists).entries()) { if (storageMetadata[i]) { const oldId = storageMetadata[i].id; this.wishlists[availableId] = storageWishlists[oldId]; wishlistsToAdd[availableId] = storageWishlists[oldId]; storageMetadata[i].id = availableId; } } this.metadata = [...this.metadata, ...storageMetadata]; } return { wishlistsToAdd, wishlistToMerge }; } isInAnyWishlist(variantId) { if (!variantId) return false; const idsArray = variantId.split(","); return this.allWishlistItems.some(x => idsArray.includes(x)); } isInWishlist(wishlistId, variantId) { const idsArray = variantId.split(","); return (this.wishlists[wishlistId] || []).some(x => idsArray.includes(x.id)); } isLoggedIn() { return !!this.customer.id; } addToWishlist(variant, wishlistId, properties) { this.allWishlistItems.push(variant.id); if (this.wishlists[wishlistId]) { this.wishlists[wishlistId] = [variant, ...this.wishlists[wishlistId]]; } else { this.wishlists[wishlistId] = [variant]; } if (properties) this.addMetadataProperties(variant.id, wishlistId, properties); this.dataConnector.addToWishlist(this.metadata, this.wishlists, variant.id, wishlistId); for (const p in this.pixels) { this.pixels[p].addToWishlist(variant); } this.updateButtonsAfterAction(variant.id, true); document.dispatchEvent(new CustomEvent(this.events.ITEM_ADDED, { detail: { variant, wishlistId } })); Shopify.analytics.publish(this.events.ITEM_ADDED, { variant, wishlistId }); } removeFromWishlist(variantId, wishlistId) { // Remove from allWishlistItems (guard against -1 index) const allIndex = this.allWishlistItems.findIndex(id => id === variantId); if (allIndex > -1) { const newAllItemsIds = [...this.allWishlistItems]; newAllItemsIds.splice(allIndex, 1); this.allWishlistItems = newAllItemsIds; } // Remove from specific wishlist (guard against -1 index) const wlIndex = (this.wishlists[wishlistId] || []).findIndex(i => i.id === variantId); if (wlIndex > -1) { const newWishlistItems = [...this.wishlists[wishlistId]]; newWishlistItems.splice(wlIndex, 1); this.wishlists[wishlistId] = newWishlistItems; } this.removeMetadataProperties(wishlistId, variantId); this.dataConnector.removeFromWishlist(this.metadata, this.wishlists, variantId, wishlistId); this.updateButtonsAfterAction(variantId, false); document.dispatchEvent(new CustomEvent(this.events.ITEM_REMOVED, { detail: { variantId, wishlistId } })); } addWishlist(name, id) { this.metadata.push({ id, name, createdAt: Date.now(), type: "WISHLIST" }); this.dataConnector.addWishlist(this.metadata, this.wishlists, id, name); } removeWishlist(id) { const wIndex = this.metadata.findIndex(w => w.id === id); if (wIndex === -1) return; const wishlistName = this.metadata[wIndex].name; this.metadata.splice(wIndex, 1); this.wishlists[id] = []; this.dataConnector.removeWishlist(this.metadata, this.wishlists, id, wishlistName); this.setAllWishlistsItems(); this.updateButtonsAfterAction("ALL", false); } findNextWishlistId() { let max = parseInt(this.settings.wishlists.maxPerCustomer); if (this.settings?.customSections?.previouslyPurchased?.enabled) max++; if (this.settings?.customSections?.abandonedCart?.enabled) max++; if (max <= this.metadata.length) return null; const takenIds = this.metadata.map(m => m.id); return Object.keys(this.wishlists).find((key) => !takenIds.includes(key) && !this.isReservedWishlistId(key)); } // Get default title for a specific wishlist ID, with fallback to generic default getDefaultTitleForId(wishlistId) { return this.translations[`atw-default-title-${wishlistId}`] || this.translations['atw-create-new-default-title'] || "My wishlist"; } isReservedWishlistId(wishlistId) { return this.reservedWishlistIds.has(wishlistId); } getValidMetafieldsConfig() { return (this.settings?.metafieldDisplays || []).filter((mf) => mf?.namespace && mf?.key); } getApiNodeByVariantId(nodesById, variantId) { if (!variantId) return null; const directNode = nodesById.get(variantId); if (directNode) return directNode; return nodesById.get(`gid://shopify/ProductVariant/${variantId}`) || null; } normalizeRoutePath(path) { if (!path) return window.Shopify.routes.root; if (path.startsWith(window.Shopify.routes.root)) return path; return `${window.Shopify.routes.root}${path.replace(/^\/+/, "")}`; } setAdditionalData() { const metafieldDisplays = this.getValidMetafieldsConfig(); const variants = Object.keys(this.wishlists).reduce((acc, currVal) => { return [...acc, ...this.wishlists[currVal]]; }, []); if (!variants?.length) return; this.apiClient.fetchOtherVariants(variants.map(v => v.id), this.customer, metafieldDisplays).then((r) => { const nodes = (r?.nodes || []).filter(Boolean); const nodesById = new Map(nodes.map((node) => [node.id, node])); for (const wishlistId in this.wishlists) { for (const v of this.wishlists[wishlistId]) { const apiVariant = this.getApiNodeByVariantId(nodesById, v.id); if (!apiVariant) continue; v.featured_image = apiVariant.featured_image; v.url = `/products/${apiVariant.product.handle}`; v.productTitle = apiVariant.product.title; v.productImage = apiVariant.product?.featuredImage?.url; v.productId = apiVariant.product?.id?.replace("gid://shopify/Product/", ""); v.available = apiVariant.availableForSale; v.sku = apiVariant.sku; v.tags = apiVariant.product.tags; v.price = apiVariant.price; v.compareAtPrice = apiVariant.compareAtPrice; v.quantityAvailable = apiVariant.quantityAvailable; v.variants = (apiVariant.product?.variants?.nodes || []).map(n => ({...n, id: formatId(n.id)})); v.productMetafields = this.extractProductMetafields(apiVariant, metafieldDisplays); } } document.dispatchEvent(new CustomEvent("fishwishlist:variants-loaded")); setTimeout(() => { document.querySelector('atw-drawer')?.renderWishlistItems(); document.querySelector('wishlists-drawer')?.renderWishlistElements(); }, 1000); }).catch(error => { console.error('[FishWishlist] Failed to fetch variant data:', error); }); } addToCart(id, quantity, properties) { const formData = { items: [{ id, quantity, properties: { '_fish_wishlist': 'drawer', ...properties } }] }; return fetch(window.Shopify.routes.root + 'cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); } requestQuote(wishlistId, note, name) { return fetch(`${this.apiClient.proxyUrl}/wishlist/quote`, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wishlistId, customer: this.customer, currency: this.currency, note, name }) }); } addMetadataProperties(variantId, wishlistId, properties) { const metaIndex = this.metadata.findIndex(m => m.id === wishlistId); if (metaIndex === -1) return; this.metadata[metaIndex] = { ...this.metadata[metaIndex], properties: { ...(this.metadata[metaIndex]?.properties || {}), [variantId]: properties } }; this.dataConnector.setMetadata(this.metadata); } removeMetadataProperties(wishlistId, variantId) { const metaIndex = this.metadata.findIndex(m => m.id === wishlistId); if (metaIndex > -1 && this.metadata[metaIndex]?.properties?.[variantId]) { delete this.metadata[metaIndex].properties[variantId]; } } changeQuantity(wishlistId, variantId, quantity) { const metaIndex = this.metadata.findIndex(m => m.id === wishlistId); if (metaIndex === -1) return; this.metadata[metaIndex] = { ...this.metadata[metaIndex], quantities: { ...(this.metadata[metaIndex]?.quantities || {}), [variantId]: quantity } }; this.dataConnector.setMetadata(this.metadata); } changeVariant(wishlistId, index, newVariantId) { this.wishlists[wishlistId][index] = { id: newVariantId }; this.dataConnector.setWishlists(this.metadata, this.wishlists, newVariantId, wishlistId, "VARIANT_CHANGE"); } enableSharedWishlist(wishlistId) { return fetch(`${this.apiClient.proxyUrl}/shared-wishlist/enable`, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wishlistId, metadata: this.metadata, items: this.wishlists[wishlistId] }) }); } disableSharedWishlist(wishlistId) { return fetch(`${this.apiClient.proxyUrl}/shared-wishlist/disable`, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wishlistId, metadata: this.metadata }) }); } // Extract productMetafields from a Storefront API variant node (mf_0, mf_1, etc.) extractProductMetafields(apiNode, metafieldDisplays = this.getValidMetafieldsConfig()) { return metafieldDisplays.map((mf, i) => { const apiMf = apiNode.product?.[`mf_${i}`]; if (!apiMf) return null; const displayValue = apiMf.reference?.title?.value || apiMf.value; return { namespace: apiMf.namespace, key: apiMf.key, value: displayValue, type: apiMf.type }; }).filter(Boolean); } unifyItem(item) { return { variants: item?.product?.variants?.nodes || item?.variants || [], compare_at_price: (item.compareAtPrice?.amount ? parseFloat(item.compareAtPrice.amount) * 100 : null) || item.compare_at_price, price: (item.price?.amount ? parseFloat(item.price.amount) * 100 : null) || item.price, title: item?.product?.title || item.productTitle || item.title, quantity_available: item.quantityAvailable || item.inventory_quantity, variantTitle: item.title?.replace("Default Title", ""), featured_image: item.featured_image?.src || item?.productImage, small_image: item.featured_image?.src_small || item?.productImage, large_image: item.featured_image?.src_large || item?.productImage, url: this.normalizeRoutePath(item.url || `/products/${item?.product?.handle}`), available: item.available ?? item.availableForSale, tags: item?.product?.tags || item?.tags || [], id: item.id, productId: item.product?.id || item.productId, productMetafields: item.productMetafields || [] }; } updateButtonsAfterAction(variantId, adding) { document.querySelectorAll('wishlist-button').forEach(el => { if (variantId === "ALL" || el.dataset?.variantId?.indexOf(variantId) > -1) { el.dataset.isAdded = variantId === "ALL" ? el.dataset.isAdded : adding; } }); document.querySelectorAll('wishlist-button-pdp,wishlist-button-social-proof').forEach(el => { if (el?.dataset?.preselectedId === variantId) { el.dataset.isAdded = adding; if (el?.socialProof?.updateCount) el.socialProof.updateCount({data: { variant: { id: variantId } }}, adding); } }); } setPixels() { const pixelConfig = this.pixels || {}; this.pixels = { ...(pixelConfig.gtm?.enabled ? { gtm: { addToWishlist: (item) => { if (!("gtag" in window)) return; const id = formatId(item?.product?.id); try { gtag("event", "add_to_wishlist", { currency: this.currency, value: item?.price?.amount, items: [{ item_id: id, item_name: item.product.title, price: item?.price?.amount }] }); } catch (e) { console.error('[FishWishlist] GTM pixel error:', e); } } }} : {}), ...(pixelConfig.meta?.enabled ? { meta: { addToWishlist: (item) => { if (!("fbq" in window)) return; const id = formatId(item?.product?.id); try { fbq('track', 'AddToWishlist', { content_ids: [id], contents: [{ id, quantity: 1 }], content_name: item.title, currency: this.currency, value: item?.price?.amount }); } catch (e) { console.error('[FishWishlist] Meta pixel error:', e); } } }} : {}) }; } }
class FishWishlistAPI { constructor(app) { this.app = app; } addToWishlist(wishlistId, variant) { if (!this.app.metadata.find(w => w.id === wishlistId)) { this.app.addWishlist(this.app.getDefaultTitleForId(wishlistId), wishlistId); } this.app.addToWishlist(variant, wishlistId); } addProperties(wishlistId, variantId, properties) { this.app.addMetadataProperties(variantId, wishlistId, properties); } } FishConfig.app.apiClient = new StorefrontApiClient(FishConfig.storefrontApi); FishConfig.app.dataConnector = FishConfig.app.storage === "METAFIELDS" ? new MetafieldDataConnector(FishConfig.dataConnector) : new LocalStorageDataConnector(FishConfig.dataConnector); const FishWishlist = new FishWishlistApp(FishConfig.app); const FishAPI = new FishWishlistAPI(FishWishlist); window.FishWishlist = FishWishlist; window.FishAPI = FishAPI; document.dispatchEvent(new CustomEvent("fishwishlist:loaded"));
// Base class for all wishlist custom elements class WishlistHTMLElement extends HTMLElement { constructor() { super() this.app = window.FishWishlist this.t = this.translate this.location = window.location } translate(key, variables) { let translation = this.app.translations[key] || `t(${key}) not found` if (variables) { Object.keys(variables).forEach(variableKey => { translation = translation.replace(`{${variableKey}}`, variables[variableKey]) }) } return translation } }
class WishlistButton extends WishlistHTMLElement { static observedAttributes = ["data-is-added", "data-variant-id"]; constructor() { super(); this.disabled = true; this.variantId = null; this.singleWishlist = this.app.settings.wishlists.maxPerCustomer === '1'; } connectedCallback() { this.render(); this.addEventListener('click', this.handleButtonClick); this.variantId = this.dataset.variantId; if(!this.variantId) { console.error('Fish Wishlist Error: Incorrect variant id on wishlist button', this); return; } this.idsArray = this.dataset.variantId.split(","); this.preselectedId = this.dataset.preselectedId; this.wishlistId = this.dataset.wishlistId; this.isMultipleVariants = this.idsArray.length > 1; // When a specific wishlist ID is set, check if variant is in that wishlist; otherwise check all wishlists if(this.wishlistId && this.app.wishlists[this.wishlistId]) { this.dataset.isAdded = this.app.isInWishlist(this.wishlistId, this.preselectedId || this.variantId); } else { this.dataset.isAdded = this.app.isInAnyWishlist(this.preselectedId || this.variantId); } this.disabled = false; } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-is-added": if((oldValue !== newValue && !this.preselectedId)) { this.dataset.isAdded = this.app.isInAnyWishlist(this.variantId); } break; case "data-variant-id": if(oldValue === newValue) { this.dataset.isAdded = this.app.isInAnyWishlist(this.variantId); } else if(oldValue && newValue) { this.variantId = newValue; this.dataset.isAdded = this.app.isInAnyWishlist(newValue); } break; default: break; } } render() { this.innerHTML = ` <span class="empty-state">${this.app.settings.icon.empty}</span> <span class="added-state" style="display: none">${this.app.settings.icon.added}</span> `; } handleButtonClick(e) { e.preventDefault(); e.stopPropagation(); const { variantId, wishlistId, preselectedId, isMultipleVariants } = this; const variantSelector = this.app.elements.addToWishlistDrawer?.variantSelector; if (variantSelector) { variantSelector.dataset.variantSelected = preselectedId ? "true" : isMultipleVariants ? "false" : "true"; } if(this.dataset.isAdded === "true") { this.removeFromWishlist(variantId, wishlistId); } else { this.addToWishlist(e.currentTarget); } } postAddAction(product, atwButton) { const atwAction = this.app.settings?.appearance?.atwAction || "OPEN_DRAWER"; if(atwAction === "OPEN_DRAWER") { this.app.elements.wishlistsDrawer.open(); } else if(atwAction === "NO_ACTION") { // No action needed } else if(atwAction === "ANIMATION") { const wishlistIcon = document.querySelector('open-wishlists-button[data-primary]'); const image = product.featured_image?.src_small; if (!wishlistIcon || !atwButton || !image) return; const flyToCartElement = document.createElement('fly-to-cart'); flyToCartElement.style.setProperty('background-image', `url(${image})`); flyToCartElement.source = atwButton; flyToCartElement.destination = wishlistIcon; setTimeout(() => { document.body.appendChild(flyToCartElement); }, 200); } else if(atwAction === "TOAST") { setTimeout(() => { this.app.elements.wishlistToast.show(); }, 0); } } addToWishlist(atwButton) { const { variantId, wishlistId, singleWishlist, isMultipleVariants, preselectedId } = this; const metafieldDisplays = this.app.settings?.metafieldDisplays || []; // When a specific wishlist ID is provided, add directly to that wishlist if(wishlistId) { // Skip if variant is already in this wishlist if(this.app.wishlists[wishlistId]?.find(i => i.id === variantId)) return; this.app.apiClient.fetchOtherVariants([variantId], this.app.customer, metafieldDisplays).then(({nodes}) => { // Create the wishlist if it doesn't exist yet, using the per-ID default title if(!this.app.metadata.find(w => w.id === wishlistId)) { this.app.addWishlist(this.app.getDefaultTitleForId(wishlistId), wishlistId); } this.app.addToWishlist({ ...nodes[0], id: variantId, productMetafields: this.app.extractProductMetafields(nodes[0]) }, wishlistId); this.app.elements.wishlistToast.setProduct(nodes[0]); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.postAddAction(nodes[0], atwButton); this.dataset.isAdded = true; this.app.helpers.setCount(); }).catch(e => { console.error('[FishWishlist] Failed to add item to specific wishlist:', e); }); return; } if(this.app.elements.addToWishlistDrawer && !singleWishlist) { this.app.elements.addToWishlistDrawer.open(this); } else if(singleWishlist && isMultipleVariants) { this.app.elements.addToWishlistDrawer.open(this); } else if(singleWishlist) { this.app.apiClient.fetchOtherVariants([variantId], this.app.customer, metafieldDisplays).then(({nodes}) => { if(this.app.metadata.length === 0) { this.app.addWishlist(this.app.getDefaultTitleForId("WL1"), "WL1"); } this.app.addToWishlist({ ...nodes[0], id: variantId, productMetafields: this.app.extractProductMetafields(nodes[0]) }, this.app.metadata[0].id); this.app.elements.wishlistToast.setProduct(nodes[0]); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.postAddAction(nodes[0], atwButton); this.dataset.isAdded = true; this.app.helpers.setCount(); }).catch(e => { console.error('[FishWishlist] Failed to add item in single wishlist mode:', e); }); } } removeFromWishlist() { const { variantId, singleWishlist, isMultipleVariants, wishlistId, preselectedId } = this; if(preselectedId && singleWishlist) { this.app.removeFromWishlist(preselectedId, this.app.metadata[0].id); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.dataset.isAdded = false; } else if(!isMultipleVariants && wishlistId) { const parentCard = this.closest('wishlist-item-card'); if(parentCard) { parentCard.removeFromWishlist(); } else { this.app.removeFromWishlist(variantId, wishlistId); this.dataset.isAdded = false; } } else if(this.app.elements.addToWishlistDrawer && !singleWishlist) { this.app.elements.addToWishlistDrawer.open(this); } else if(singleWishlist && isMultipleVariants) { this.app.elements.addToWishlistDrawer.open(this); } else if(singleWishlist) { this.app.removeFromWishlist(variantId, this.app.metadata[0].id); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.dataset.isAdded = false; } this.app.helpers.setCount(); } } customElements.define('wishlist-button', WishlistButton);
class WishlistButtonPDP extends WishlistButton { static observedAttributes = ["data-is-added", "data-variant-id"]; constructor() { super(); this.updateAfterVariantChange = this.updateAfterVariantChange.bind(this) this.addEventListeners() } updateAfterVariantChange(variant) { if (!variant?.id) return const newVariantId = variant.id.toString() this.dataset.preselectedId = newVariantId this.dataset.isAdded = this.app.allWishlistItems.indexOf(newVariantId) > -1 } addEventListeners() { if ("subscribe" in window) { try { subscribe(PUB_SUB_EVENTS?.variantChange, (event) => { this.updateAfterVariantChange(event?.data?.variant) }) } catch (error) { console.error('[FishWishlist] PDP variant change subscription error:', error) } } try { if (window.jQuery) { window.jQuery(document).on("onVariantChange", (e, v) => { this.updateAfterVariantChange(v) }) } } catch (error) { console.error('[FishWishlist] jQuery variant change error:', error) } } render() { let addLabel = this.t('atw-button-pdp') let addedLabel = this.t('atw-button-pdp-added') if (addLabel.indexOf('not found') > -1) addLabel = "" if (addedLabel.indexOf('not found') > -1) addedLabel = "" this.innerHTML = ` <span class="empty-state">${this.app.settings.icon.empty}<span class="fish-pdp-label">${addLabel}</span></span> <span class="added-state" style="display: none">${this.app.settings.icon.added}<span class="fish-pdp-label">${addedLabel}</span></span> ` } } customElements.define('wishlist-button-pdp', WishlistButtonPDP)
class WishlistsSocialProof extends WishlistHTMLElement { constructor() { super(); this.addEventListeners(); this.counts = this.parseDatasetJson("counts", []); this.product = this.parseDatasetJson("product", { tags: [] }); this.countField = this.querySelector('.count'); this.nonZeroContainer = this.querySelector('.count-none-zero'); this.zeroContainer = this.querySelector('.count-zero'); this.wishlistButton = this.querySelector('wishlist-button-social-proof'); if(this.wishlistButton) { this.updateCount({ data: { variant: { id: this.wishlistButton.dataset.preselectedId } } }, true); } this.handleTagActions(); this.classList.add("loaded"); } parseDatasetJson(key, fallback) { const rawValue = this.dataset[key]; if (!rawValue) return fallback; try { return JSON.parse(rawValue); } catch (error) { console.error(`[FishWishlist] Invalid social proof dataset "${key}"`, error); return fallback; } } handleTagActions() { if(!this.app.settings.tagActions) return; const actions = this.app.settings.tagActions.filter(a => a.type === "SOCIAL_PROOF"); for (const action of actions) { const { tag, selectedAction } = action; if(this.product.tags.indexOf(tag) > -1) { switch (selectedAction) { case "HIDE_WIDGET": this.style["display"] = "none"; break; default: break; } } } } addEventListeners() { if("subscribe" in window) { try { subscribe(PUB_SUB_EVENTS?.variantChange, (event) => { this.updateCount(event, true); }); } catch (error) { console.error('[FishWishlist] Social proof variant subscription error:', error); } } } updateCount(event, adding) { const newVariantId = event?.data?.variant?.id.toString(); const newCount = this.counts.find(c => c.id === newVariantId); const isAdded = this.app.allWishlistItems.findIndex(el => el === newVariantId) > -1; const c = newCount?.count + (isAdded && adding ? 1 : 0); if( c > 0 ) { this.nonZeroContainer.classList.remove("is-hidden"); this.zeroContainer.classList.add("is-hidden"); if(this.countField) this.countField.innerHTML = c; } else { this.nonZeroContainer.classList.add("is-hidden"); this.zeroContainer.classList.remove("is-hidden"); } } } customElements.define('wishlists-social-proof', WishlistsSocialProof); class WishlistButtonSocialProof extends WishlistButton { static observedAttributes = ["data-is-added", "data-variant-id"]; constructor() { super(); this.socialProof = this.closest('wishlists-social-proof'); this.addEventListeners(); } addEventListeners() { if("subscribe" in window) { try { subscribe(PUB_SUB_EVENTS?.variantChange, (event) => { const newVariantId = event?.data?.variant?.id.toString(); this.dataset.preselectedId = newVariantId; this.dataset.isAdded = this.app.allWishlistItems.indexOf(newVariantId) > -1; }); } catch (error) { console.error('[FishWishlist] Social proof button variant subscription error:', error); } } } render() { let addLabel = this.t('social-proof-button-add-label'); let addedLabel = this.t('social-proof-button-added-label'); if(addLabel.indexOf('not found') > -1) addLabel = ""; if(addedLabel.indexOf('not found') > -1) addedLabel = ""; this.innerHTML = ` <span class="empty-state">${this.app.settings.icon.empty}${addLabel}</span> <span class="added-state" style="display: none">${this.app.settings.icon.added}${addedLabel}</span> `; } } customElements.define('wishlist-button-social-proof', WishlistButtonSocialProof);
class WishlistVariantSelector extends WishlistHTMLElement { static observedAttributes = ["data-current-variant-id"]; constructor() { super(); this.selector = this.querySelector('select'); this.titles = this.querySelector('.wishlist-variant-selector__titles'); this.images = this.querySelector('.wishlist-variant-selector__images'); } connectedCallback() { this.selector.addEventListener('change', this.handleSelectionChange.bind(this)); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-current-variant-id": this.handleCurrentVariantChange(newValue); break; default: break; } } setSelectedOption(newVariantId) { this.selectedOption = this.availableOptions.find(v => v.id.indexOf(newVariantId) > -1); } showSelectedVariant(newVariantId) { this.querySelectorAll(`[data-variant-id]`).forEach(el => { el.classList.add("is-hidden"); }); this.querySelectorAll(`[data-variant-id="${newVariantId}"]`).forEach(el => { el.classList.remove("is-hidden"); }); } handleCurrentVariantChange(newVariantId) { this.app.elements.addToWishlistDrawer.setCurrentVariantId(newVariantId); this.setSelectedOption(newVariantId); this.showSelectedVariant(newVariantId); } handleSelectionChange(event) { if(!event.target.value) { this.dataset.variantSelected = "false"; return; } this.dataset.variantSelected = "true"; this.dataset.currentVariantId = event.target.value; const placeholderOption = this.selector.querySelector('option:first-child[value=""]'); if(placeholderOption) placeholderOption.disabled = true; } async render(variantIds) { const metafieldDisplays = this.app.settings?.metafieldDisplays || []; let { nodes } = await this.app.apiClient.fetchOtherVariants(variantIds, this.app.customer, metafieldDisplays); nodes = nodes.filter(n => n); if(nodes.length === 0) this.innerHTML = "Invalid variant id"; // Enrich nodes with productMetafields so form-based add paths have the data nodes.forEach(n => { n.productMetafields = this.app.extractProductMetafields(n); }); this.availableOptions = nodes; this.variants = nodes; this.images.innerHTML = this.titles.innerHTML = ""; for (const node of nodes) { const legacyId = node.id.replace("gid://shopify/ProductVariant/", ""); this.selector.querySelector(`[value="${legacyId}"]`).innerHTML = node.title; this.images.innerHTML += `<img src="${node.featured_image?.src || ''}" data-variant-id="${legacyId}" class="is-hidden"/>`; this.titles.innerHTML += `<span data-variant-id="${legacyId}" class="is-hidden"> ${node.product.title} </span>`; } } buildOptions(variantIds, preselectedId) { if(preselectedId) { this.selector.innerHTML = variantIds.map(id => `<option ${preselectedId === id ? 'selected="selected"' : ''} value="${id}">${id}</option>`).join(""); } else { this.selector.innerHTML = `<option value="">${this.t('atw-select-variant-option-placeholder')}</option>` + variantIds.map(id => `<option value="${id}">${id}</option>`).join(""); } } } customElements.define('wishlist-variant-selector', WishlistVariantSelector);
class MarketingOptIn extends WishlistHTMLElement { constructor() { super(); this.form = this.querySelector('form'); this.submitButton = this.querySelector('button[type="submit"]'); } connectedCallback() { this.form.addEventListener('submit', this.handleSubmit.bind(this)); } handleSubmit(event) { event.preventDefault(); event.stopPropagation(); this.submitButton.classList.add('submitting'); const formData = new FormData(event.target); const { wishlistMarketingOptIn, wishlistMarketingOptInSMS } = Object.fromEntries(formData); if(!wishlistMarketingOptIn && !wishlistMarketingOptInSMS) { this.submitButton.classList.remove('submitting'); return; } this.querySelector('.success-message').classList.remove('is-hidden'); fetch(`/apps/fish-wishlist/customer/subscribe`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ wishlistMarketingOptIn, wishlistMarketingOptInSMS }) }).catch(e => { console.error('Failed to subscribe:', e); }).finally(() => { this.submitButton.classList.remove('submitting'); }); Shopify.analytics.visitor( { email: FishWishlist.customer.email }, { appId: "157109354497" }, ); setTimeout(() => { this.remove(); }, 1000); } } customElements.define('marketing-opt-in', MarketingOptIn); class NewWishlistForm extends WishlistHTMLElement { constructor() { super(); this.errorContainer = this.querySelector('.error-message'); this.form = this.querySelector('form'); this.input = this.querySelector('input'); this.button = this.querySelector('input[type="submit"]'); } connectedCallback() { this.form.addEventListener('submit', this.handleSubmit.bind(this)); this.input.addEventListener('input', this.handleInputChange.bind(this)); } // Get default title for the next available wishlist ID getDefaultTitleForNextId() { const nextId = this.app.findNextWishlistId(); if(!nextId) return null; return this.app.getDefaultTitleForId(nextId); } // Pre-fill input with per-ID default title setDefaultTitle() { const defaultTitle = this.getDefaultTitleForNextId(); if(defaultTitle && !this.input.value) { this.input.value = defaultTitle; this.button.disabled = false; } } handleInputChange(event) { this.button.disabled = !event.target.value; } handleSubmit(event) { event.preventDefault(); event.stopPropagation(); const formData = new FormData(event.target); const { wishlistName } = Object.fromEntries(formData); const selectedOption = this.app.elements.addToWishlistDrawer.variantSelector.selectedOption; const variantId = formatId(selectedOption.id); const wishlistId = this.app.findNextWishlistId(); if(!wishlistId) { this.errorContainer.innerHTML = this.t("wishlists-limit-eached-error"); return; } this.errorContainer.innerHTML = ""; if(!variantId || !wishlistId || !wishlistName) return; this.app.addWishlist(wishlistName, wishlistId); this.app.addToWishlist({ ...selectedOption, id: variantId }, wishlistId); this.app.elements.addToWishlistDrawer.trigger.dataset.isAdded = true; this.app.elements.addToWishlistDrawer.renderWishlistItems(); this.app.elements.addToWishlistDrawer.setCurrentVariantId(variantId); this.app.elements.addToWishlistDrawer.classList.remove("empty"); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.querySelector('input').value = ""; this.app.helpers.setCount(); // Pre-fill with default title for the next available slot this.setDefaultTitle(); } } customElements.define('new-wishlist-form', NewWishlistForm); class AddToWishlistForm extends WishlistHTMLElement { static observedAttributes = ["data-current-variant-id"]; constructor() { super(); this.form = this.querySelector('form'); this.cta = this.querySelector('.button--cta'); this.ctaRemove = this.querySelector('.button--cta-remove'); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-current-variant-id": this.handleCurrentVariantIdChange(newValue); break; } } handleCurrentVariantIdChange(newId) { this.cta.disabled = !!this.app.wishlists[this.dataset.wishlistId]?.find(i => i.id === newId); } connectedCallback() { this.form.addEventListener('submit', this.handleSubmit.bind(this)); this.ctaRemove.addEventListener('click', this.handleCtaRemoveClick.bind(this)); } handleCtaRemoveClick(event) { event.preventDefault(); event.stopPropagation(); const variantId = this.dataset.currentVariantId; if(!variantId) return; const wishlistId = this.dataset.wishlistId; this.app.removeFromWishlist(variantId, wishlistId); this.app.elements.addToWishlistDrawer.trigger.dataset.isAdded = false; this.cta.disabled = false; this.app.elements.wishlistsDrawer.renderWishlistElements(); this.app.helpers.setCount(); } handleSubmit(event) { event.preventDefault(); event.stopPropagation(); let wishlistId = this.dataset.wishlistId; if(!this.app.metadata.find(w => w.id === wishlistId)) { if(this.app.metadata?.[0]?.id) { wishlistId = this.app.metadata[0].id; } else { const formData = new FormData(event.target); const { wishlistName } = Object.fromEntries(formData); this.app.addWishlist(wishlistName, wishlistId); } } const selectedOption = this.app.elements.addToWishlistDrawer.variantSelector.selectedOption; this.app.addToWishlist({ ...selectedOption, id: this.dataset.currentVariantId }, wishlistId); this.cta.disabled = true; this.app.elements.wishlistsDrawer.renderWishlistElements(); this.app.elements.addToWishlistDrawer.trigger.dataset.isAdded = true; this.app.helpers.setCount(); } } customElements.define('add-to-wishlist-form', AddToWishlistForm);
class RequestQuoteButton extends WishlistHTMLElement { constructor() { super(); this.addEventListener('click', this.handleClick.bind(this)); } reset() { this.innerHTML = `<span>${this.t("b2b-request-a-quote-button-label")}</span>`; this.removeAttribute('disabled'); } handleClick() { const submittingClass = "submitting"; if(this.classList.contains(submittingClass)) return; this.classList.add(submittingClass); const note = this.parentNode.querySelector(`textarea[data-wishlist-id="${this.dataset.wishlistId}"]`)?.value; const wishlistName = this.app.metadata.find(m => m.id === this.dataset.wishlistId)?.name; const name = this.app.settings.wishlists.maxPerCustomer !== '1' ? wishlistName : null; this.app.requestQuote(this.dataset.wishlistId, note, name).then(r => r.json()).then(r => { this.setAttribute('disabled', "disabled"); this.innerHTML = this.t("b2b-quote-sent-confirmation", {quote_id: r?.draftOrder?.name}); }).catch(e => { console.error('[FishWishlist] Failed to request quote:', e); }).finally(() => { this.classList.remove(submittingClass); }); } } customElements.define('request-quote-button', RequestQuoteButton); // Buy All button - adds all wishlist items to cart at once class BuyAllButton extends WishlistHTMLElement { constructor() { super(); this.addEventListener('click', this.handleClick.bind(this)); } reset() { this.innerHTML = `<span>${this.t("buy-all-button-label")}</span>`; this.removeAttribute('disabled'); this.classList.remove('submitted'); } handleClick() { if(this.classList.contains('submitting')) return; this.classList.add('submitting'); const wishlistId = this.dataset.wishlistId; const items = this.app.wishlists?.[wishlistId] || []; const meta = this.app.metadata.find(m => m.id === wishlistId); // Only add items that are available for sale const cartItems = items.filter(item => item.available).map(item => { let quantity = 1; if(this.app.settings.b2b.enableQuantityPickers) quantity = meta?.quantities?.[item.id] || 1; const properties = meta?.properties?.[item.id] || null; return { id: item.id, quantity, properties: { '_fish_wishlist': 'drawer', ...properties } }; }); if(!cartItems.length) { this.classList.remove('submitting'); return; } const formData = { items: cartItems }; fetch(window.Shopify.routes.root + 'cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }).then(r => r.json()).then(jsonRes => { if(!jsonRes.items) { console.error(jsonRes.message); } else { this.classList.add('submitted'); this.setAttribute('disabled', 'disabled'); this.innerHTML = `<span>${this.t("atc-button-added")}</span>`; document.dispatchEvent(new CustomEvent(this.app.events.ITEM_ADDED_TO_CART, { detail: { items: jsonRes.items } })); } }).catch(e => { console.error(e); }).finally(() => { this.classList.remove('submitting'); }); } } customElements.define('buy-all-button', BuyAllButton); class QuoteQuantityPicker extends WishlistHTMLElement { constructor() { super(); this.quantityContainer = this.querySelector('.quote-quantity-picker__current'); this.querySelector('button[name="plus"]').addEventListener('click', this.add.bind(this)); this.querySelector('button[name="minus"]').addEventListener('click', this.remove.bind(this)); this.currentQuantity = this.getInitialQuantity(); this.render(); } getInitialQuantity() { let quantity = 1; const qs = this.app.metadata.find(m => m.id === this.dataset.wishlistId)?.quantities; if(qs) { quantity = qs[this.dataset.variantId] || 1; } return quantity; } render() { this.quantityContainer.innerHTML = this.currentQuantity; } changeQuantity(newQuantity) { if(newQuantity > 0) { this.currentQuantity = newQuantity; this.app.changeQuantity(this.dataset.wishlistId, this.dataset.variantId, newQuantity); this.render(); } this.closest('wishlist-element')?.resetQuoteButton(); } add() { this.changeQuantity(this.currentQuantity + 1); } remove() { this.changeQuantity(this.currentQuantity - 1); } } customElements.define('quote-quantity-picker', QuoteQuantityPicker);
class ShareWishlistButton extends WishlistHTMLElement { constructor() { super(); this.addEventListener('click', this.handleClick.bind(this)); } handleClick() { const submittingClass = "submitting"; const wId = this.dataset.wishlistId; if(this.classList.contains(submittingClass)) return; this.classList.add(submittingClass); const wMetadata = this.app.metadata.find(m => m.id === wId); const updateFunction = wMetadata?.sharedWishlist?.id ? this.app.disableSharedWishlist.bind(this.app) : this.app.enableSharedWishlist.bind(this.app); updateFunction(wId).then(r => r.json()).then(({metadata}) => { if(metadata) this.app.metadata = metadata; this.app.elements.wishlistsDrawer.renderWishlistElements(); }).catch(e => { console.error('[FishWishlist] Failed to update shared wishlist:', e); }).finally(() => { this.classList.remove(submittingClass); }); } } customElements.define('share-wishlist-button', ShareWishlistButton); class FishWishlistShareButton extends WishlistHTMLElement { constructor() { super(); this.elements = { shareButton: this.querySelector('button.button--share'), copyButton: this.querySelector('button.button--copy') }; this.url = this.dataset.url; this.title = this.dataset.title; this.init(); } init() { this.elements.shareButton.addEventListener('click', this.handleShareClick.bind(this)); this.elements.copyButton.addEventListener('click', this.handleCopyClick.bind(this)); } handleShareClick() { if(navigator.share) { navigator.share({ url: this.url, title: this.title }).catch(e => { console.error('Share failed:', e); }); } } handleCopyClick() { navigator.clipboard.writeText(this.url).then(() => { alert("Copied to clipboard!"); }).catch(e => { console.error('Copy failed:', e); }); } } customElements.define('fish-wishlist-share-button', FishWishlistShareButton);
class AddToWishlistDrawerItem extends WishlistHTMLElement { static observedAttributes = ["data-current-variant-id"]; constructor() { super(); this.wishlistCount = this.querySelector('.items-count'); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-current-variant-id": this.handleCurrentVariantIdChange(newValue); break; } } connectedCallback() { this.render(); } handleCurrentVariantIdChange(newId) { const isAdded = !!this.wishlistItems?.find(i => i.id === newId); this.dataset.isAdded = isAdded; this.cta.disabled = isAdded; } render() { const wMeta = this.app.metadata.find(w => w.id === this.dataset.id); this.wishlistItems = this.app.wishlists[this.dataset.id]; const wishlistSize = this.wishlistItems.length; this.innerHTML = `<div class="atw-drawer__item-header"> <div class="atw-drawer__item-name"> <open-wishlists-button role="button" data-id="${this.dataset.id}" class="atw-drawer__open"> <span class="wishlist-title bold"> ${wMeta.name}</span> <span class="atw-drawer__item-in-list">${this.t("atw-item-in-list")} </span></open-wishlists-button> <p class="items-count"> ${wishlistSize === 0 ? this.t("atw-no-items-label") : wishlistSize === 1 ? this.t("atw-items-singular")?.replace("{{count}}", wishlistSize) : this.t("atw-items-plural")?.replace("{{count}}", wishlistSize)} </p> <div class="wishlist-item__row"> <div class="wishlist-item__placeholders-container">${ this.wishlistItems.slice(0, 4).map(item => `<div class="wishlist-item__placeholder"><img src="${item.featured_image?.src || item.productImage || ''}" srcset="${item.featured_image?.src || item.productImage} 28w" width="28" height="35"></div>`).join("") }${ this.wishlistItems.length > 4 ? `<div class="wishlist-item__placeholder">+${this.wishlistItems.length - 4}</div>` : '' }</div> </div> </div> <div class="atw-drawer__item-cta"> <button type="button" class="${this.app.settings.classes.primaryButton} button--small button--cta" aria-label="Add to ${wMeta.name}"> <span class="item-in-list">${this.t("atw-item-in-list")}</span> <span class="add-to-list">${this.t("atw-cta-button")}</span> </button> <button type="button" class="${this.app.settings.classes.secondaryButton} button--cta-remove" aria-label="Remove from ${wMeta.name}"> ${this.t("atw-remove-from-list")} </button> <open-wishlists-button role="button" data-id="${this.dataset.id}" class="fish-wishlist-button--link button--link link--text">${this.app.elements.icons.view}${this.t('atw-view-list-button-label')}</open-wishlists-button> </div> </div>`; this.cta = this.querySelector('.button--cta'); this.ctaRemove = this.querySelector('.button--cta-remove'); this.openWishlistsButtons = this.querySelectorAll('open-wishlists-button'); this.addEventListeners(); } addEventListeners() { if(this.cta) this.cta.addEventListener('click', this.handleCtaClick.bind(this)); if(this.ctaRemove) this.ctaRemove.addEventListener('click', this.handleCtaRemoveClick.bind(this)); } handleCtaClick() { const variantId = this.dataset.currentVariantId; if(!variantId) return; const wishlistId = this.dataset.id; this.app.addToWishlist({ ...this.app.elements.addToWishlistDrawer.variantSelector.selectedOption, id: variantId }, wishlistId); this.app.elements.addToWishlistDrawer.trigger.dataset.isAdded = true; this.render(variantId); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.handleCurrentVariantIdChange(variantId); this.app.helpers.setCount(); } handleCtaRemoveClick() { const variantId = this.dataset.currentVariantId; if(!variantId) return; const wishlistId = this.dataset.id; this.app.removeFromWishlist(variantId, wishlistId); this.app.elements.addToWishlistDrawer.trigger.dataset.isAdded = false; this.render(); this.app.elements.wishlistsDrawer.renderWishlistElements(); this.handleCurrentVariantIdChange(variantId); this.app.helpers.setCount(); } openWishlistsDrawer() { // Intentionally empty - reserved for future implementation } } customElements.define('atw-drawer-item', AddToWishlistDrawerItem);
class AddToWishlistDrawer extends WishlistHTMLElement { static observedAttributes = ["data-open"]; constructor() { super(); this.addEventListener('keyup', (evt) => evt.code === 'Escape' && this.close()); this.variantSelector = this.querySelector('wishlist-variant-selector'); this.addToWishlistDrawerItemsContainer = this.querySelector('.atw-drawer-items__container'); this.newWishlistForm = this.querySelector('new-wishlist-form'); this.addToWishlistForm = this.querySelector('add-to-wishlist-form'); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-open": this.handleOpenChange(newValue); break; default: break; } } connectedCallback() { this.renderWishlistItems(); } renderWishlistItems() { if(!this.app.elements?.addToWishlistDrawer) return; const currentVariantId = this.app.elements.addToWishlistDrawer.variantSelector.dataset.currentVariantId; if(!this.addToWishlistDrawerItemsContainer) { return this.addToWishlistDrawerItems = {}; } this.addToWishlistDrawerItemsContainer.innerHTML = this.app.metadata.filter(m => m.type === "WISHLIST").map(w => `<atw-drawer-item class="wishlist-card wishlist-card--wishlist" data-id="${w.id}"> </atw-drawer-item>`).join(""); const currentWishlists = [...this.querySelectorAll('atw-drawer-item')]; if(currentWishlists.length === 0) this.classList.add("empty"); else if(currentWishlists.length === 15) this.classList.add("full"); else this.classList.remove("empty", "full"); this.addToWishlistDrawerItems = currentWishlists.reduce((acc, currVal) => { currVal.dataset.currentVariantId = currentVariantId; acc[currVal.dataset.id] = currVal; return acc; }, {}); } async handleOpenChange(value) { if(value === "true") { if(this.variantSelector) { this.variantSelector.buildOptions(this.variantIds, this.preselectedId); await this.variantSelector.render(this.variantIds, this.preselectedId); } this.classList.add("active"); if(this.variantIds.length > 1) this.classList.add("show-variant-selector"); this.setCurrentVariantId(this.preselectedId || this.variantIds[0]); }else if(value === "false") { this.classList.remove("show-variant-selector"); this.classList.remove("active"); } } setCurrentVariantId(id) { if(this.addToWishlistDrawerItems) { Object.keys(this.addToWishlistDrawerItems).forEach(key => { this.addToWishlistDrawerItems[key].dataset.currentVariantId = id; }); } this.variantSelector.setSelectedOption(id); this.variantSelector.showSelectedVariant(id); if(this.addToWishlistForm) this.addToWishlistForm.dataset.currentVariantId = id; } open(trigger) { this.trigger = trigger; this.variantIds = this.trigger.dataset.variantId.split(","); this.preselectedId = this.trigger.dataset.preselectedId; this.dataset.open = "true"; document.body.classList.add('overflow-hidden'); document.documentElement.classList.add('lock'); // Pre-fill new wishlist form with per-ID default title if(this.newWishlistForm) this.newWishlistForm.setDefaultTitle(); } close() { this.dataset.open = "false"; document.body.classList.remove('overflow-hidden'); document.documentElement.classList.remove('lock'); } toggle() { if(this.dataset.open === "false") this.open(); else this.close(); } } customElements.define('atw-drawer', AddToWishlistDrawer);
class WishlistItemCard extends WishlistHTMLElement { constructor() { super(); this.wishlistElement = this.closest('wishlist-element'); } addEventListeners() { if(this.cta) this.cta.addEventListener('click', this.addToCart.bind(this)); if(this.ctaRemove) this.ctaRemove.addEventListener('click', this.removeFromWishlist.bind(this)); if(this.variantSelector) this.variantSelector.addEventListener('change', this.changeVariant.bind(this)); } connectedCallback() { this.render(); } removeFromWishlist() { const variantId = this.dataset.id, wEl = this.wishlistElement; if(!variantId) return; const wishlistId = this.dataset.wishlistId; this.app.removeFromWishlist(variantId, wishlistId); this.app.elements.addToWishlistDrawer.renderWishlistItems(); this.remove(); this.app.helpers.setCount(); if(!wEl) return; wEl.setData(); wEl.renderHeader(wEl.itemsCount); wEl.renderBody(wEl.itemsCount); if(wEl.itemsCount === 0) wEl.renderItemCards(0); wEl.addEventListeners(); wEl?.resetQuoteButton(); } addToCart() { if(this.cta.onclick) return; this.cta.innerHTML = `<span>${this.t("atc-button-loading")}</span>`; this.cta.classList.add("submitting"); const meta = this.app.metadata.find(m => m.id === this.dataset.wishlistId); let quantity = 1; if(this.app.settings.b2b.enableQuantityPickers) quantity = meta?.quantities?.[this.dataset.id] || 1; const properties = meta?.properties?.[this.dataset.id] || null; this.app.addToCart(this.dataset.id, quantity, properties).then(r => { return r.json(); }).then(jsonRes => { document.dispatchEvent(new CustomEvent(this.app.events.ITEM_ADDED_TO_CART, { detail: { variantId: this.dataset.id, quantity } })); if(!jsonRes.items) { this.errorContainer.innerHTML = jsonRes.message; this.cta.innerHTML = `<span>${this.t("atc-button")}</span>`; } else { this.cta.innerHTML = `<span>${this.t("atc-button-added")}</span>`; try { if("publish" in window) { publish(PUB_SUB_EVENTS?.cartUpdate, { source: 'product-form', cartData: jsonRes.items[0], productVariantId: this.dataset.id }); } const cartBubbles = this.app.theme.elements.cartBubble; const updateFunction = this.app.theme.functions.cartUpdate; if(cartBubbles?.length) { cartBubbles.forEach((b) => { b.innerHTML = parseInt(b.innerHTML || 0) + quantity; }); } if(updateFunction) updateFunction({ lineNo: 1, quantity: jsonRes?.items[0]?.quantity, quantityAdded: quantity }); } catch (error) { console.log(error); } } }).catch((error) => { this.errorContainer.innerHTML = error.message; console.error('Error:', error); }).finally(() => { this.cta.classList.remove("submitting"); }); } changeVariant(event) { const index = this.dataset.index; const newId = event.target.value; this.app.changeVariant(this.dataset.wishlistId, index, newId); this.dataset.id = newId; this.querySelectorAll('.item-card__image img, .price-container > p').forEach(el => { el.classList.add("is-hidden"); }); this.querySelectorAll(`.item-card__image img[data-id="${newId}"], .price-container p[data-id="${newId}"]`).forEach(el => { el.classList.remove("is-hidden"); }); this.refreshAtcButton(this.item); } refreshAtcButton(item) { const html = this.buildAtcButton(item); const template = document.createElement("template"); template.innerHTML = html.trim(); this.querySelector(".button--cta").replaceWith(template.content.firstChild.cloneNode(true)); this.cta = this.querySelector('.button--cta'); this.addEventListeners(); } buildAtcButton(item) { let isDisabled = !item.available; let isHidden = false; let label = item.available ? this.t("atc-button") : this.t("sold-out-button"); let onclick = false; const tagActions = this.app.settings.tagActions; if(tagActions) { for (const action of tagActions) { const {tag, selectedAction, userInput} = action; if(item.tags.indexOf(tag) > -1 || tag === "*") { switch (selectedAction) { case "RENAME_LABEL": label = userInput; break; case "HIDE_BUTTON": isHidden = true; break; case "DISABLE_BUTTON": isDisabled = true; break; case "REDIRECT_AFTER_CLICK": onclick = `event.preventDefault();event.stopPropagation();window.location.href = "${userInput.replace("{{product-url}}", item.url)}"`; break; case "RUN_JAVASCRIPT": onclick = `let item = ${JSON.stringify(item)}; ${userInput}; return false;`; break; default: break; } } } } if(isHidden) return ``; return `<button ${onclick ? `onclick='${onclick}'` : ''} ${isDisabled ? 'disabled="disabled"' : ''} class="${this.app.settings.classes.primaryButton} button--small ${!isDisabled ? 'button--cta' : ''}"><span>${label}</span></button>`; } getPriceHtml(item, outerHtml, isOtherVariant = false) { const compareAtPrice = (item.compareAtPrice?.amount || item.compare_at_price) * (isOtherVariant ? 100 : 1); const price = (item?.price?.amount || item?.price) * (isOtherVariant ? 100 : 1); const id = formatId(item.id); return `${outerHtml ? `<p data-id="${id}">` : ''} ${compareAtPrice && compareAtPrice > price ? `<span class="was-price fish-wishlist-color--tertiary">${Shopify.formatMoney(compareAtPrice, Shopify.money_format)}</span>` : ''} <span> ${Shopify.formatMoney(price, Shopify.money_format)} </span> ${outerHtml ? '</p>' : ''}`; } getVariantSelector(imgSize) { const variants = this.item?.product?.variants?.nodes || this.item?.variants; // Check if price should be hidden based on tag actions (shared by all variants of same product) let hidePrice = false; const item = this.app.unifyItem(this.item); const tagActions = this.app.settings.tagActions; if(tagActions) { for (const action of tagActions) { const {tag, selectedAction} = action; if(action.type === "WISHLIST_ITEM" && (item.tags.indexOf(tag) > -1 || tag === "*")) { if(selectedAction === "HIDE_PRICE") { hidePrice = true; break; } } } } this.app.apiClient.fetchOtherVariants(variants.map(v => formatId(v.id))).then(({nodes}) => { for (const node of nodes) { if(node.id.indexOf(this.item.id) > -1) continue; const imgEl = document.createElement(`img`); const price = document.createElement(`p`); imgEl.dataset.id = price.dataset.id = formatId(node.id); imgEl.classList.add("is-hidden"), price.classList.add("is-hidden"); imgEl.loading = "lazy"; imgEl.src = imgSize === 500 ? node?.featured_image?.src_large : imgSize === 350 ? node?.featured_image?.src : node?.featured_image?.src_small; imgEl.width = imgSize; // Only add price HTML if price should not be hidden if(!hidePrice) { price.innerHTML = this.getPriceHtml(node, false, true); } this.querySelector('.item-card__image').append(imgEl); this.querySelector('.price-container').append(price); } }).catch((error) => { console.error('Error fetching other variants:', error); }); return `<div class="fish-select"> <select class="fish-card-variant-selector select__select"> ${(() => variants.map(v => { const id = formatId(v.id); return `<option ${id === this.item.id ? 'selected="selected"' : ''} value="${id}">${v.title}</option>`; }).join(""))()} </select> ${(() => this.app.elements.icons.caret)()} </div>`; } render() { let { settings, customer, wishlists, metadata } = this.app; const quantityPickersEnabled = settings?.b2b?.enableQuantityPickers && customer?.id; const variantSelectorEnable = settings?.b2b?.enableVariantSelector; const wishlistId = this.dataset.wishlistId; const variantId = this.dataset.id; this.wishlistItems = wishlists[wishlistId]; metadata = metadata.find(m => m.id === wishlistId); const properties = metadata?.properties?.[variantId]; this.item = this.wishlistItems.find(i => i.id == variantId); const itemId = this.item.id; const item = this.app.unifyItem(this.item); const isGrid = settings.appearance.wishlistItemsView === 'grid'; const isWishlistPage = window.location.pathname.includes('/customer-wishlist/'); const imgSize = isWishlistPage ? 500 : isGrid ? 350 : 80; const imgUrl = isWishlistPage ? item.large_image : isGrid ? item.featured_image : item.small_image; // Build metafield display HTML from configured product metafields const metafieldsHTML = item.productMetafields?.length ? item.productMetafields.map(mf => `<p class="item-card__metafield fish-wishlist-color--tertiary">${mf.value}</p>` ).join('') : ''; const selectorHTML = variantSelectorEnable && item?.variants?.length > 1 ? this.getVariantSelector(imgSize) : (() => `<p class="item-card__variant-title fish-wishlist-color--tertiary">${item.variantTitle || ""}</p>`)(); const propertiesHTML = properties ? (() => Object.keys(properties) .filter(key => key[0] !== "_") .map(key => `<div class="product-option"><dt>${key}:</dt> <dd>${properties[key]}</dd></div>`) .join(""))() : ''; const quantityPickersHTML = quantityPickersEnabled ? (() => ` <quote-quantity-picker data-wishlist-id="${wishlistId}" data-variant-id="${itemId}"> <button name="minus">-</button> <span class="quote-quantity-picker__current"></span> <button name="plus">+</button> </quote-quantity-picker>` )() : ''; // Check if price should be hidden based on tag actions let hidePrice = false; const itemTagActions = this.app.settings.tagActions; if(itemTagActions) { for (const action of itemTagActions) { const {tag, selectedAction} = action; if(action.type === "WISHLIST_ITEM" && (item.tags.indexOf(tag) > -1 || tag === "*")) { if(selectedAction === "HIDE_PRICE") { hidePrice = true; break; } } } } const priceHTML = hidePrice ? '' : this.getPriceHtml(item, true); this.innerHTML = `<div class="wishlist-drawer__wishlist-item-card"> <div class="item-card__image"> <wishlist-button class="position-absolute top-right" data-variant-id="${itemId}" data-wishlist-id="${wishlistId}"></wishlist-button> <img data-id="${item.id}" loading="lazy" src="${imgUrl || ''}" width="${imgSize}"> </div> <div class="item-card__details"> <span class="fish-wishlist-color--primary"> <a href="${item.url}?variant=${itemId}"> ${item.title} </a> </span> ${selectorHTML} ${propertiesHTML} ${metafieldsHTML} <div class="price-container"> ${priceHTML} </div> ${quantityPickersHTML} <div class="atc-error"></div> </div> <div class="item-card__atc-container"> ${this.buildAtcButton(item)} <a role="button" tabindex="0" class="fish-wishlist-button--link button--link button--cta-remove" aria-label="Remove from list"> <span>${this.t("remove-from-list")}</span> </a> </div> </div>`; this.cta = this.querySelector('.button--cta'); this.ctaRemove = this.querySelector('.button--cta-remove'); this.errorContainer = this.querySelector('.atc-error'); this.variantSelector = this.querySelector('.fish-card-variant-selector'); this.addEventListeners(); } } customElements.define('wishlist-item-card', WishlistItemCard);
class WishlistsDrawer extends WishlistHTMLElement { static observedAttributes = ["data-open"]; constructor() { super(); this.addEventListener('keyup', (evt) => evt.code === 'Escape' && this.close()); this.wishlistsDrawerElementsContainer = this.querySelector('.wishlists-drawer__wishlists-container'); this.backButton = this.querySelector('.wishlists-drawer__back'); this.renderWishlistElements(); } getElementInnerContent(w) { const hasItems = this.app.wishlists?.[w.id]?.length; const sharedWishlistEnabled = hasItems && this.app.settings?.shareWishlist?.enable; const shareIcon = this.app.elements.icons.share; const clipboardIcon = this.app.elements.icons.clipboard; return `<div class="wishlist-drawer__item-header"></div> <div class="wishlist-drawer__item-body"></div> <div class="wishlist-drawer__wishlist-items-cards-container wishlist-items-cards-container--${this.app.settings.appearance?.wishlistItemsView}"></div> ${(() => { if(!w?.sharedWishlist?.id || !sharedWishlistEnabled) return ''; const url = `${window.location.origin}/apps/fish-wishlist/shared-wishlist/${w?.sharedWishlist.handle}`; return `<fish-wishlist-share-button data-url="${url}" data-title="${w.name}"> <button class="fish-wishlist-share-button fish-wishlist-button--link button--share">${shareIcon}<span>${this.t("send-share-link-button-label")}</span></button> <button class="fish-wishlist-share-button button--copy">${clipboardIcon}<span>${this.t("copy-share-link-button-label")}</span></button></fish-wishlist-share-button>`; })()} ` + (this.app.settings?.b2b?.enableQuoteNoteField && hasItems && this.app.customer?.id && w.type === 'WISHLIST' ? `<textarea data-wishlist-id="${w.id}" class="fish-textarea quote-note" placeholder="${this.t("b2b-quote-note-placeholder")}"></textarea>` : '') + ((() => { const showQuote = this.app.settings?.b2b?.enableRequestQuoteButton && hasItems && this.app.customer?.id && w.type === 'WISHLIST'; const showBuyAll = this.app.settings?.b2b?.enableBuyAllButton && hasItems && w.type === 'WISHLIST'; const bothEnabled = showQuote && showBuyAll; // When both buttons are enabled, wrap them in a row container sharing 50% width each if(bothEnabled) { return `<div class="wishlist-drawer__action-buttons-row">` + `<request-quote-button data-wishlist-id="${w.id}" role="button" class="${this.app.settings.classes.primaryButton}"><span>${this.t("b2b-request-a-quote-button-label")}</span></request-quote-button>` + `<buy-all-button data-wishlist-id="${w.id}" role="button" class="${this.app.settings.classes.primaryButton}"><span>${this.t("buy-all-button-label")}</span></buy-all-button>` + `</div>`; } return (showQuote ? `<request-quote-button data-wishlist-id="${w.id}" role="button" class="${this.app.settings.classes.primaryButton}"><span>${this.t("b2b-request-a-quote-button-label")}</span></request-quote-button>` : '') + (showBuyAll ? `<buy-all-button data-wishlist-id="${w.id}" role="button" class="${this.app.settings.classes.primaryButton}"><span>${this.t("buy-all-button-label")}</span></buy-all-button>` : ''); })()) + (sharedWishlistEnabled && this.app.customer?.id ? `<share-wishlist-button data-wishlist-id="${w.id}" role="button" class="${this.app.settings.classes.secondaryButton}"><span>${!w?.sharedWishlist?.id ? this.t("share-list-button-label") : this.t("unshare-list-button-label")}</span></share-wishlist-button>` : ''); } renderWishlistElements() { if(!this.wishlistsDrawerElementsContainer) return; this.wishlistsDrawerElementsContainer.innerHTML = this.app.metadata.map(w => `<wishlist-element class="wishlist-card wishlist-card--${w.type?.toLowerCase()}" data-id="${w.id}"> ${this.getElementInnerContent(w)} </wishlist-element>`).join(""); const currentWishlistElements = [...this.querySelectorAll('wishlist-element')]; if(currentWishlistElements.length === 0) this.classList.add("empty"); else this.classList.remove("empty"); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "data-open": this.handleOpenChange(newValue); break; default: break; } } async handleOpenChange(value) { if(value === "true") { this.classList.add("active"); } else if(value === "false") { this.classList.remove("active"); } } open() { this.dataset.open = "true"; document.body.classList.add('overflow-hidden'); document.documentElement.classList.add('lock'); } close() { this.dataset.open = "false"; document.body.classList.remove('overflow-hidden'); document.documentElement.classList.remove('lock'); } closeAll() { this.close(); this.app.elements.addToWishlistDrawer.close(); } toggle() { if(this.dataset.open === "false") this.open(); else this.close(); } } customElements.define('wishlists-drawer', WishlistsDrawer);
class WishlistElement extends WishlistHTMLElement { static observedAttributes = ["data-variants"]; constructor() { super(); } setElements() { this.headerContainer = this.querySelector(".wishlist-drawer__item-header"); this.bodyContainer = this.querySelector(".wishlist-drawer__item-body"); this.cardsContainer = this.querySelector(".wishlist-drawer__wishlist-items-cards-container"); } setData() { this.wishlistItems = this.app.wishlists[this.dataset.id]; this.wishlistMetadata = this.app.metadata.find(w => w.id === this.dataset.id); this.itemsCount = this.wishlistItems?.length; } connectedCallback() { this.setData(); this.setElements(); this.render(); } addEventListeners() { this.cta.addEventListener('click', this.toggleItemCards.bind(this)); this.deleteWishlistButton.addEventListener('click', this.deleteWishlist.bind(this)); } renderHeader(itemsCount) { this.headerContainer.innerHTML = `<div class="wishlist-drawer__item-name"> <span class="wishlist-title bold"> ${this.wishlistMetadata.name}</span> <p class="items-count"> ${itemsCount === 0 ? this.t("no-items-label") : itemsCount === 1 ? this.t("items-singular")?.replace("{{count}}", itemsCount) : this.t("items-plural")?.replace("{{count}}", itemsCount)} </p> </div> <button class="wishlist-drawer__delete-button"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" focusable="false" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9.8 3.5V1.87a.47.47 0 0 0-.47-.47H4.67a.47.47 0 0 0-.47.47V3.5m5.6 0H4.2m5.6 0h1.4m1.4 0h-1.4m-7 0H2.8m-1.4 0h1.4m0 0v8.63c0 .26.21.47.47.47h7.46c.26 0 .47-.21.47-.47V3.5M5.594 6.3v3.5M8.4 6.3v3.5"></path></svg> </button>`; this.deleteWishlistButton = this.querySelector('.wishlist-drawer__delete-button'); } renderBody(itemsCount) { this.bodyContainer.innerHTML = `<div class="wishlist-drawer__bottom-row"> <div class="wishlist-item__placeholders-container"> ${(() => { return this.wishlistItems.slice(0, 4).map(item => `<div class="wishlist-item__placeholder"><img src="${item.featured_image?.src || item.productImage || ''}" srcset="${item.featured_image?.src || item.productImage} 28w" width="28" height="35"></div>`).join(""); })()} ${(() => { return itemsCount > 4 ? `<div class="wishlist-item__placeholder fish-wishlist-color--tertiary">+${itemsCount - 4}</div>` : ''; })()} </div> <div class="wishlist-drawer__item-cta"> <a ${itemsCount === 0 ? 'disabled="disabled"' :''} role="button" class="fish-wishlist-button--link button--link link--text button--cta" aria-label="View items in ${this.wishlistMetadata.name}"> <span class="show-copy">${this.app.elements.icons.view}${this.t("show-items-btn")}</span> <span class="hide-copy">${this.app.elements.icons.hide}${this.t("hide-items-btn")}</span> </a> </div> </div>`; this.cta = this.querySelector('.button--cta'); } renderItemCards(itemsCount) { if(itemsCount === 0) { this.cardsContainer.classList.add('empty'); this.cardsContainer.innerHTML = `<p class="wishlist-item-card__empty">${this.t("empty-wishlist")} </p>`; } else { this.cardsContainer.innerHTML = this.wishlistItems.map((item, i) => `<wishlist-item-card data-index="${i}" data-wishlist-id="${this.dataset.id}" data-id="${item.id}"> </wishlist-item-card>`).join(""); const currentWishlistElements = [...this.querySelectorAll('wishlist-item-card')]; this.wishlistItemsCards = currentWishlistElements.reduce((acc, currVal) => { acc[currVal.dataset.id] = currVal; return acc; }, {}); } } render(expand = false) { if(!this.app.elements?.wishlistsDrawer) return; this.setData(); this.innerHTML = this.app.elements.wishlistsDrawer.getElementInnerContent({id: this.dataset.id, ...this?.wishlistMetadata}); this.setElements(); this.renderHeader(this.itemsCount); this.renderBody(this.itemsCount); this.renderItemCards(this.itemsCount); this.addEventListeners(); if(expand) this.showItemCards(); } deleteWishlist() { this.app.removeWishlist(this.dataset.id); this.remove(); this.app.elements.addToWishlistDrawer.renderWishlistItems(); } toggleItemCards() { if(this.classList.contains("expanded")) { this.hideItemCards(); } else { this.showItemCards(); } } showItemCards() { this.renderItemCards(this.itemsCount); this.classList.add('expanded'); } hideItemCards() { this.classList.remove('expanded'); } resetQuoteButton() { this.querySelector('request-quote-button')?.reset(); this.querySelector('buy-all-button')?.reset(); } } customElements.define('wishlist-element', WishlistElement);
class OpenWishlistsButton extends WishlistHTMLElement { constructor() { super(); } connectedCallback() { // Cache the count bubble reference once children are available in the DOM this.wishlistCountBubble = this.querySelector('.wishlist-count-bubble'); this.addEventListener('click', this.handleButtonClick); this.setCount(); } setCount() { // Re-query in case the element wasn't cached yet (e.g. dynamically inserted children) if(!this.wishlistCountBubble) this.wishlistCountBubble = this.querySelector('.wishlist-count-bubble'); if(!this.wishlistCountBubble) return; const count = this.app.allWishlistItems.length; if (count > 99) { this.wishlistCountBubble.innerHTML = '<span>99+</span>'; } else { this.wishlistCountBubble.innerHTML = count ? `<span>${count}</span>` : ''; } } handleButtonClick() { if(this.dataset.id) { this.app.elements.wishlistsDrawer.querySelectorAll(`wishlist-element`).forEach(el => { el.hideItemCards(); }); this.app.elements.wishlistsDrawer.querySelector(`wishlist-element[data-id="${this.dataset.id}"]`)?.showItemCards(); setTimeout(() => { this.app.elements.wishlistsDrawer.toggle(); }, 100); this.app.elements.wishlistsDrawer.backButton.classList.remove('is-hidden'); } else if(this.dataset.redirect) { window.location.href = this.dataset.redirect; } else { this.app.elements.wishlistsDrawer.backButton.classList.add('is-hidden'); this.app.elements.wishlistsDrawer.toggle(); } } } customElements.define('open-wishlists-button', OpenWishlistsButton);
class WishlistToast extends WishlistHTMLElement { constructor() { super(); this.app = window.FishWishlist; this.product = null; } connectedCallback() { this.render(); } show() { if(!this.product) return; this.classList.add('active'); setTimeout(() => { this.classList.remove('active'); this.product = null; }, 2000); } setProduct(product) { this.product = product; this.render(); } render() { if(!this.product) return; this.innerHTML = `<div class="fish-toast position-fixed top-right"> <div class="fish-toast__content"> <div class="fish-toast__product-image"> <img src="${this?.product?.featured_image?.src_small || this?.product?.productImage || ''}" alt="${this?.product?.title}"> </div> <p class="fish-toast__message"> <span class="fish-product-title"> ${this?.product?.product?.title} </span> ${this.t("wishlist-toast-added-message")}</p> </div> </div>`; } } customElements.define('wishlist-toast', WishlistToast);
class FlyToCart extends HTMLElement { /** @type {Element} */ source; /** @type {Element} */ destination; connectedCallback() { this.#animate(); } onAnimationEnd(elements, callback, options = { subtree: true }) { const animations = Array.isArray(elements) ? elements.flatMap((element) => element.getAnimations(options)) : elements.getAnimations(options); const animationPromises = animations.reduce((acc, animation) => { // Ignore ViewTimeline animations if (animation.timeline instanceof DocumentTimeline) { acc.push(animation.finished); } return acc; }, /** @type {Promise<Animation>[]} */ ([])); return Promise.allSettled(animationPromises).then(callback); } #animate() { const rect = this.getBoundingClientRect(); const sourceRect = this.source.getBoundingClientRect(); const destinationRect = this.destination.getBoundingClientRect(); // Define bezier curve points const offset = { x: rect.width / 2, y: rect.height / 2, }; const startPoint = { x: sourceRect.left + sourceRect.width / 2 - offset.x, y: sourceRect.top + sourceRect.height / 2 - offset.y, }; const endPoint = { x: destinationRect.left + destinationRect.width / 2 - offset.x, y: destinationRect.top + destinationRect.height / 2 - offset.y, }; // Calculate the control points const controlPoint1 = { x: startPoint.x, y: startPoint.y - 200 }; const controlPoint2 = { x: endPoint.x - 300, y: endPoint.y - 100 }; /** @type {number | null} */ let startTime = null; const duration = 600; this.style.opacity = '1'; /** * Animates the flying thingy along the bezier curve. * @param {number} currentTime - The current time. */ const animate = (currentTime) => { if (!startTime) startTime = currentTime; const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Calculate current position along the bezier curve const position = bezierPoint(progress, startPoint, controlPoint1, controlPoint2, endPoint); // Update the position of the flying thingy this.style.setProperty('--x', `${position.x}px`); this.style.setProperty('--y', `${position.y}px`); // Scale down as it approaches the cart const scale = 1 - progress * 0.5; this.style.setProperty('--scale', `${scale}`); // Continue the animation if not finished if (progress < 1) { requestAnimationFrame(animate); } else { // Fade out the flying thingy this.style.opacity = '0'; this.onAnimationEnd(this, () => this.remove()); } }; // Position the flying thingy back to the start point this.style.setProperty('--x', `${startPoint.x}px`); this.style.setProperty('--y', `${startPoint.y}px`); // Start the animation requestAnimationFrame(animate); } } /** * Calculates a point on a cubic Bezier curve. * @param {number} t - The parameter value (0 <= t <= 1). * @param {{x: number, y: number}} p0 - The starting point (x, y). * @param {{x: number, y: number}} p1 - The first control point (x, y). * @param {{x: number, y: number}} p2 - The second control point (x, y). * @param {{x: number, y: number}} p3 - The ending point (x, y). * @returns {{x: number, y: number}} The point on the curve. */ function bezierPoint(t, p0, p1, p2, p3) { const cX = 3 * (p1.x - p0.x); const bX = 3 * (p2.x - p1.x) - cX; const aX = p3.x - p0.x - cX - bX; const cY = 3 * (p1.y - p0.y); const bY = 3 * (p2.y - p1.y) - cY; const aY = p3.y - p0.y - cY - bY; const x = aX * Math.pow(t, 3) + bX * Math.pow(t, 2) + cX * t + p0.x; const y = aY * Math.pow(t, 3) + bY * Math.pow(t, 2) + cY * t + p0.y; return { x, y }; } if (!customElements.get('fly-to-cart')) { customElements.define('fly-to-cart', FlyToCart); }
// Self-hosted HTML initialization // Populates translations, injects icons, and toggles // single/multi-wishlist visibility from FishConfig. (function initSelfHostedHTML() { const t = FishWishlist.translations || {}; const settings = FishWishlist.settings || {}; const customer = FishWishlist.customer || {}; const metadata = FishWishlist.metadata || []; const maxWishlists = parseInt(settings.wishlists?.maxPerCustomer || 1); const isSingleWishlist = maxWishlists === 1; const isLoggedIn = !!customer.id; const limitToLoggedIn = settings.wishlists?.limitToLoggedInCustomers || false; const showWishlists = !limitToLoggedIn || isLoggedIn; const classes = settings.classes || {}; // Populate translations document.querySelectorAll('[data-t]').forEach((el) => { const key = el.dataset.t; if (t[key]) el.textContent = t[key]; }); document.querySelectorAll('[data-t-html]').forEach((el) => { const key = el.dataset.tHtml; if (t[key]) el.innerHTML = t[key]; }); document.querySelectorAll('[data-t-placeholder]').forEach((el) => { const key = el.dataset.tPlaceholder; if (t[key]) el.placeholder = t[key]; }); document.querySelectorAll('[data-t-value]').forEach((el) => { const key = el.dataset.tValue; if (t[key]) el.value = t[key]; }); // Inject icons from FishConfig const icons = FishWishlist.elements?.icons || {}; document.querySelectorAll('.sh-caret-icon').forEach((el) => { if (icons.caret) el.innerHTML = icons.caret; }); document.querySelectorAll('.sh-view-icon').forEach((el) => { if (icons.view) el.innerHTML = icons.view; }); // Apply button classes from settings document.querySelectorAll('[data-class-primary]').forEach((el) => { if (classes.primaryButton) el.classList.add(...classes.primaryButton.split(' ').filter(Boolean)); }); document.querySelectorAll('[data-class-secondary]').forEach((el) => { if (classes.secondaryButton) el.classList.add(...classes.secondaryButton.split(' ').filter(Boolean)); }); // ATW Drawer: show one mode, remove the other from DOM entirely // so the AddToWishlistDrawer constructor finds only one wishlist-variant-selector const atwDrawer = document.querySelector('atw-drawer'); if (isSingleWishlist) { // Remove multi-wishlist wrapper, show single document.querySelector('.sh-multi-wishlist')?.remove(); document.querySelectorAll('.sh-single-wishlist').forEach((el) => { el.style.display = ''; }); if (atwDrawer) atwDrawer.classList.add('atw-drawer--single-wishlist'); const form = document.querySelector('add-to-wishlist-form'); const firstId = metadata[0]?.id || 'WL1'; if (form) form.dataset.wishlistId = firstId; const nameInput = document.querySelector('#add-to-wishlist-form input[name="wishlistName"]'); if (nameInput) nameInput.value = t['atw-create-new-default-title'] || 'My wishlist'; const owlBtn = document.querySelector('.sh-single-wishlist open-wishlists-button'); if (owlBtn) owlBtn.dataset.id = firstId; if (!showWishlists) { if (owlBtn) owlBtn.style.display = 'none'; const loginLabel = document.querySelector('.sh-login-to-view'); if (loginLabel) loginLabel.style.display = ''; } } else { // Remove single-wishlist wrapper, show multi document.querySelector('.sh-single-wishlist')?.remove(); if (showWishlists) { document.querySelectorAll('.sh-multi-wishlist').forEach((el) => { el.style.display = ''; }); } } if (metadata.length === 0 && atwDrawer) atwDrawer.classList.add('empty'); // Re-assign drawer element references after removing the inactive wrapper // (the constructor may have grabbed references from the now-removed wrapper) if (atwDrawer) { atwDrawer.variantSelector = atwDrawer.querySelector('wishlist-variant-selector'); atwDrawer.addToWishlistDrawerItemsContainer = atwDrawer.querySelector('.atw-drawer-items__container'); atwDrawer.newWishlistForm = atwDrawer.querySelector('new-wishlist-form'); atwDrawer.addToWishlistForm = atwDrawer.querySelector('add-to-wishlist-form'); // Also register on app.elements so renderWishlistItems can access it FishWishlist.elements.addToWishlistDrawer = atwDrawer; atwDrawer.renderWishlistItems(); } // Register wishlists drawer on app.elements and configure const wishlistsDrawer = document.querySelector('wishlists-drawer'); if (wishlistsDrawer) { FishWishlist.elements.wishlistsDrawer = wishlistsDrawer; if (metadata.length === 0) wishlistsDrawer.classList.add('empty'); if (isSingleWishlist) wishlistsDrawer.classList.add('wishlists-drawer--single-wishlist'); } // Login Banner — shown for logged-out customers whenever the translation exists. // limitToLoggedIn forces visibility even when the drawer is .empty (matches default extension). if (!isLoggedIn && t['login-msg']) { document.querySelectorAll('.customer-login-banner').forEach((el) => { el.style.display = limitToLoggedIn ? 'block' : ''; }); } // Marketing Opt-In const showEmailOptIn = settings.misc?.showMarketingOptIn || false; const showSmsOptIn = settings.misc?.showMarketingOptInSMS || false; if (isLoggedIn && !customer.acceptsMarketing && (showEmailOptIn || showSmsOptIn)) { const optIn = document.querySelector('marketing-opt-in'); if (optIn) { optIn.style.display = ''; if (showEmailOptIn) { const emailRow = optIn.querySelector('.sh-email-consent'); if (emailRow) emailRow.style.display = ''; } if (showSmsOptIn && customer.phone) { const smsRow = optIn.querySelector('.sh-sms-consent'); if (smsRow) smsRow.style.display = ''; } } } })();