@occluze/tether 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +320 -365
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +242 -491
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +66 -88
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +36 -33
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +28 -36
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1270 -894
- package/dist/tether.markets.js +73 -90
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +44 -68
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -23
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/{order-lines.js → order-lines.ts} +20 -41
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/{page-events.js → page-events.ts} +55 -90
- package/src/account/server-client.ts +183 -0
- package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/{account-pages.js → account-pages.ts} +29 -37
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
- package/src/analytics.ts +315 -0
- package/src/{api-version.js → api-version.ts} +25 -31
- package/src/{astro.js → astro.ts} +110 -104
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/{drawer.js → drawer.ts} +18 -31
- package/src/cart/mutations.ts +237 -0
- package/src/cart/{render.js → render.ts} +68 -96
- package/src/cart/store.ts +279 -0
- package/src/{cart.js → cart.ts} +82 -128
- package/src/client.ts +391 -0
- package/src/{collection.js → collection.ts} +30 -38
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
- package/src/controls/{discount.js → discount.ts} +13 -34
- package/src/controls/{extras.js → extras.ts} +15 -24
- package/src/controls/{navigation.js → navigation.ts} +24 -30
- package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
- package/src/{diagnose.js → diagnose.ts} +95 -96
- package/src/dom.ts +268 -0
- package/src/{extras.js → extras.ts} +43 -52
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/{images.js → images.ts} +30 -43
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/{loader.js → loader.ts} +149 -123
- package/src/market-codes.ts +6 -0
- package/src/{markets-ui.js → markets-ui.ts} +91 -68
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/{mounting.js → mounting.ts} +88 -101
- package/src/{page-market.js → page-market.ts} +32 -38
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/{product-events.js → product-events.ts} +33 -48
- package/src/{product-render.js → product-render.ts} +38 -77
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/{product.js → product.ts} +90 -126
- package/src/{repeaters.js → repeaters.ts} +40 -52
- package/src/{search.js → search.ts} +85 -103
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/{shopify-values.js → shopify-values.ts} +24 -22
- package/src/{status.js → status.ts} +34 -54
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
- package/src/{variant-render.js → variant-render.ts} +24 -50
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics.js +0 -352
- package/src/cart/checkout.js +0 -97
- package/src/cart/mutations.js +0 -234
- package/src/cart/store.js +0 -326
- package/src/client.js +0 -267
- package/src/config.js +0 -349
- package/src/dom.js +0 -287
- package/src/fragments.js +0 -237
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/market-codes.js +0 -35
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-state.js +0 -318
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
package/src/config.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The runtime configuration. `cfg` is one shared object that every browser module imports; `configure()` replaces its
|
|
3
|
-
* contents in place, so those imports see the new values, and `setCountry()` changes its market keys the same way.
|
|
4
|
-
* Options that end up in query text or attribute names are validated here, once, where they arrive.
|
|
5
|
-
*/
|
|
6
|
-
/** @import { Money } from './shopify-values.js' */
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {object} MarketsConfig Where the visitor's market comes from (`markets.js`).
|
|
10
|
-
* @property {boolean | ('saved' | 'server' | 'webflow')[]} [detect] true: every source; a list: only those.
|
|
11
|
-
* @property {Record<string, { country?: string, language?: string, locale?: string }>} [locales] Webflow locale
|
|
12
|
-
* (subdirectory or html lang) to market.
|
|
13
|
-
* @property {string} [cookie] The cookie a server sets with the visitor's country.
|
|
14
|
-
* @property {boolean} [remember] Remember the visitor's choice (default true).
|
|
15
|
-
* @property {boolean} [strict] With `locales`: ignore an html lang that isn't in the map.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {object} AnalyticsConfig The optional analytics part.
|
|
20
|
-
* @property {boolean | string} [dataLayer] GA4/GTM events, into `window.dataLayer` or the named array.
|
|
21
|
-
* @property {boolean | { consent?: 'no-banner' | 'default-banner' | 'custom-banner', proxy?: string }} [shopify]
|
|
22
|
-
* Shopify's own analytics.
|
|
23
|
-
* @property {(name: string, data: Record<string, unknown>) => void} [onEvent]
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {object} AccountConfig The optional customer accounts part.
|
|
28
|
-
* @property {string} [clientId] Browser mode: the Customer Account API client id.
|
|
29
|
-
* @property {string} [server] Server mode: the Worker's account routes, a URL or a path on this site.
|
|
30
|
-
* @property {string} [orderUrl] The site's own order page, with `{id}` for the numeric order id.
|
|
31
|
-
* @property {string} [subscriptionsUrl] Shopify's subscription management page.
|
|
32
|
-
* @property {string} [paymentUrl] A separate page for payment methods (default `subscriptionsUrl`).
|
|
33
|
-
* @property {string} [callbackUrl] Where Shopify sends the visitor after login (default the current page).
|
|
34
|
-
* @property {string} [logoutUrl] Where Shopify sends the visitor after logout (default the home page).
|
|
35
|
-
* @property {boolean} [remember] Keep the login across tabs and restarts (localStorage, not sessionStorage).
|
|
36
|
-
* @property {string} [tokenUrl] The site's own token endpoint, for when the browser can't call Shopify's.
|
|
37
|
-
* @property {string} [shopDomain] The *.myshopify.com domain, when `domain` is another one.
|
|
38
|
-
* @property {string} [locale] The language of Shopify's login pages.
|
|
39
|
-
* @property {(url: string) => void} [navigate] Replaces `location.assign` for the login redirect (popups, tests).
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/** @typedef {{ namespace: string, key: string }} MetafieldIdentifier */
|
|
43
|
-
/** @typedef {string | MetafieldIdentifier} MetafieldRef `'namespace.key'` or the identifier itself. */
|
|
44
|
-
/** @typedef {{ code: string, message: string, target?: string }} CartWarning Shopify's note on a cart change. */
|
|
45
|
-
/** @typedef {'_self' | '_blank'} CheckoutTarget */
|
|
46
|
-
/** @typedef {'one-time' | 'subscription'} PurchaseKind */
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {object} TetherConfig Every option, with its default filled in.
|
|
50
|
-
* @property {string} domain The store's domain, without protocol or path.
|
|
51
|
-
* @property {string} token The public Storefront API access token.
|
|
52
|
-
* @property {string} apiVersion A dated Shopify API version.
|
|
53
|
-
* @property {string | undefined} country An ISO country code for `@inContext(country:)`.
|
|
54
|
-
* @property {string | undefined} language A language for `@inContext(language:)`.
|
|
55
|
-
* @property {string | undefined} locale An `Intl` locale for money and dates.
|
|
56
|
-
* @property {MarketsConfig | null} markets
|
|
57
|
-
* @property {string} prefix The attribute prefix: `tether` reads `tether-product`, `tether-bind`.
|
|
58
|
-
* @property {string} productPath A folder (`/product`) or a template with `{handle}` / `{id}`.
|
|
59
|
-
* @property {string} collectionPath
|
|
60
|
-
* @property {MetafieldRef[]} metafields The product metafields to fetch.
|
|
61
|
-
* @property {number} imagesFirst
|
|
62
|
-
* @property {number} variantsFirst
|
|
63
|
-
* @property {number} sellingPlanGroupsFirst
|
|
64
|
-
* @property {number} sellingPlansFirst
|
|
65
|
-
* @property {number} collectionFirst
|
|
66
|
-
* @property {number} retries Retries of a request that failed on the network, a 429/5xx or Shopify's throttling.
|
|
67
|
-
* @property {boolean} revalidate Refresh server-rendered products once in the background.
|
|
68
|
-
* @property {string} storageKey The localStorage key of the cart id (default `<prefix>:cart:<domain>`).
|
|
69
|
-
* @property {string} cartDrawerOpenClass
|
|
70
|
-
* @property {boolean} openCartOnAdd
|
|
71
|
-
* @property {boolean} cartFocus Move focus into the open cart, keep Tab inside it, restore it on close.
|
|
72
|
-
* @property {CheckoutTarget} checkoutTarget
|
|
73
|
-
* @property {PurchaseKind} defaultPurchase
|
|
74
|
-
* @property {boolean} variantUrl `?variant=` follows the visitor's choice on the page's main product.
|
|
75
|
-
* @property {number} maxAge Seconds a Shopify answer may be cached; 0 leaves it to Shopify, which can be hours.
|
|
76
|
-
* @property {number} lowStock `tether-show="low-stock"` at this many or fewer (needs inventory access).
|
|
77
|
-
* @property {number} searchDelay Milliseconds the search part waits after the last keystroke.
|
|
78
|
-
* @property {boolean} autoMount Mount the document on DOMContentLoaded.
|
|
79
|
-
* @property {boolean} observe Mount elements added later (CMS load more, filters).
|
|
80
|
-
* @property {boolean} astro Mount on Astro's `astro:page-load` and close the cart on navigation.
|
|
81
|
-
* @property {string | null} partsUrl The folder of the optional parts, for the `<script>` build.
|
|
82
|
-
* @property {AnalyticsConfig | null} analytics
|
|
83
|
-
* @property {AccountConfig | null} account
|
|
84
|
-
* @property {((warning: CartWarning) => string | null | undefined) | null} warningText The site's text for a cart
|
|
85
|
-
* warning; nothing keeps Shopify's.
|
|
86
|
-
* @property {((money: Money, locale: string | undefined) => string) | null} formatMoney
|
|
87
|
-
* @property {((reason: string) => void) | null} onLayoutChange Default: `ScrollTrigger.refresh()` when present.
|
|
88
|
-
* @property {((drawers: HTMLElement[]) => void) | null} onCartOpen
|
|
89
|
-
* @property {((drawers: HTMLElement[]) => void) | null} onCartClose
|
|
90
|
-
* @property {((node: HTMLElement) => void) | null} onLineEnter
|
|
91
|
-
* @property {((node: HTMLElement, done: () => void) => void) | null} onLineLeave Animates a line out, then calls
|
|
92
|
-
* `done()`.
|
|
93
|
-
* @property {((element: HTMLElement, previousText: string) => void) | null} onValueChange
|
|
94
|
-
* @property {((error: unknown, context: string) => void) | null} onError Every failure that reaches the visitor.
|
|
95
|
-
* @property {boolean} debug
|
|
96
|
-
*/
|
|
97
|
-
|
|
98
|
-
/** @typedef {{ [Key in keyof TetherConfig]?: TetherConfig[Key] }} TetherOptions */
|
|
99
|
-
|
|
100
|
-
/** @type {Readonly<TetherConfig>} */
|
|
101
|
-
export const DEFAULTS = {
|
|
102
|
-
domain: '',
|
|
103
|
-
token: '',
|
|
104
|
-
apiVersion: '2026-07',
|
|
105
|
-
country: undefined,
|
|
106
|
-
language: undefined,
|
|
107
|
-
locale: undefined,
|
|
108
|
-
markets: null,
|
|
109
|
-
prefix: 'tether',
|
|
110
|
-
productPath: '/product',
|
|
111
|
-
collectionPath: '/collection',
|
|
112
|
-
metafields: [],
|
|
113
|
-
imagesFirst: 25,
|
|
114
|
-
variantsFirst: 100,
|
|
115
|
-
sellingPlanGroupsFirst: 10,
|
|
116
|
-
sellingPlansFirst: 25,
|
|
117
|
-
collectionFirst: 24,
|
|
118
|
-
retries: 2,
|
|
119
|
-
revalidate: true,
|
|
120
|
-
storageKey: '',
|
|
121
|
-
cartDrawerOpenClass: 'is-open',
|
|
122
|
-
openCartOnAdd: true,
|
|
123
|
-
cartFocus: true,
|
|
124
|
-
checkoutTarget: '_self',
|
|
125
|
-
defaultPurchase: 'one-time',
|
|
126
|
-
variantUrl: false,
|
|
127
|
-
maxAge: 60,
|
|
128
|
-
lowStock: 5,
|
|
129
|
-
searchDelay: 150,
|
|
130
|
-
autoMount: true,
|
|
131
|
-
observe: false,
|
|
132
|
-
astro: true,
|
|
133
|
-
partsUrl: null,
|
|
134
|
-
analytics: null,
|
|
135
|
-
account: null,
|
|
136
|
-
warningText: null,
|
|
137
|
-
formatMoney: null,
|
|
138
|
-
onLayoutChange: null,
|
|
139
|
-
onCartOpen: null,
|
|
140
|
-
onCartClose: null,
|
|
141
|
-
onLineEnter: null,
|
|
142
|
-
onLineLeave: null,
|
|
143
|
-
onValueChange: null,
|
|
144
|
-
onError: null,
|
|
145
|
-
debug: false,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/** @type {TetherConfig} */
|
|
149
|
-
export const cfg = { ...DEFAULTS };
|
|
150
|
-
|
|
151
|
-
/** The prefix becomes part of attribute names, CSS selectors and event names, so it is kept to those characters. */
|
|
152
|
-
const PREFIX = /^[a-z][a-z0-9-]*$/;
|
|
153
|
-
/**
|
|
154
|
-
* Metafield identifiers are written into the product fragment's text: a GraphQL fragment can't declare variables, so
|
|
155
|
-
* they can't be sent as variables. Only these characters are let through, which covers Shopify's namespaces and keys.
|
|
156
|
-
*/
|
|
157
|
-
const METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
|
|
158
|
-
/** A namespace is the same, or an app-reserved one: `$app` or `$app:<name>`. */
|
|
159
|
-
const METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @typedef {'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst' | 'collectionFirst'
|
|
163
|
-
* | 'retries' | 'maxAge' | 'lowStock'} CountOption
|
|
164
|
-
*/
|
|
165
|
-
/**
|
|
166
|
-
* @typedef {'warningText' | 'formatMoney' | 'onLayoutChange' | 'onCartOpen' | 'onCartClose' | 'onLineEnter'
|
|
167
|
-
* | 'onLineLeave' | 'onValueChange' | 'onError'} HookOption
|
|
168
|
-
*/
|
|
169
|
-
|
|
170
|
-
/** @type {readonly CountOption[]} Options that are written into queries as list sizes or used as counts. */
|
|
171
|
-
const COUNT_OPTIONS = [
|
|
172
|
-
'imagesFirst',
|
|
173
|
-
'variantsFirst',
|
|
174
|
-
'sellingPlanGroupsFirst',
|
|
175
|
-
'sellingPlansFirst',
|
|
176
|
-
'collectionFirst',
|
|
177
|
-
'retries',
|
|
178
|
-
'maxAge',
|
|
179
|
-
'lowStock',
|
|
180
|
-
];
|
|
181
|
-
/** @type {readonly HookOption[]} */
|
|
182
|
-
const HOOK_OPTIONS = [
|
|
183
|
-
'warningText',
|
|
184
|
-
'formatMoney',
|
|
185
|
-
'onLayoutChange',
|
|
186
|
-
'onCartOpen',
|
|
187
|
-
'onCartClose',
|
|
188
|
-
'onLineEnter',
|
|
189
|
-
'onLineLeave',
|
|
190
|
-
'onValueChange',
|
|
191
|
-
'onError',
|
|
192
|
-
];
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* @template T
|
|
196
|
-
* @param {string} name
|
|
197
|
-
* @param {unknown} value
|
|
198
|
-
* @param {T} fallback
|
|
199
|
-
* @returns {T} `fallback`, with a warning unless `value` was left out.
|
|
200
|
-
*/
|
|
201
|
-
function invalidOption(name, value, fallback) {
|
|
202
|
-
if (value !== undefined) warn(`invalid ${name} ${JSON.stringify(value)}: using ${JSON.stringify(fallback)}`);
|
|
203
|
-
return fallback;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* @param {CountOption} name
|
|
208
|
-
* @param {unknown} value
|
|
209
|
-
* @returns {number} `value` when it is a non-negative integer, else the default.
|
|
210
|
-
*/
|
|
211
|
-
function countOption(name, value) {
|
|
212
|
-
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return value;
|
|
213
|
-
return invalidOption(name, value, DEFAULTS[name]);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* @template {string} Choice
|
|
218
|
-
* @param {string} name
|
|
219
|
-
* @param {unknown} value
|
|
220
|
-
* @param {readonly [Choice, ...Choice[]]} choices The first one is the default.
|
|
221
|
-
* @returns {Choice}
|
|
222
|
-
*/
|
|
223
|
-
function choiceOption(name, value, choices) {
|
|
224
|
-
return choices.find((choice) => choice === value) ?? invalidOption(name, value, choices[0]);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @param {unknown} value
|
|
229
|
-
* @returns {string} The store's domain without protocol or path: `https://shop.example/x` → `shop.example`.
|
|
230
|
-
*/
|
|
231
|
-
function domainOption(value) {
|
|
232
|
-
if (typeof value !== 'string') return '';
|
|
233
|
-
return value.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* @param {unknown} ref
|
|
238
|
-
* @returns {MetafieldIdentifier | null} The identifier of `'namespace.key'` or `{ namespace, key }`, when both parts are
|
|
239
|
-
* valid identifiers.
|
|
240
|
-
*/
|
|
241
|
-
export function toMetafieldIdentifier(ref) {
|
|
242
|
-
/** @type {unknown} */
|
|
243
|
-
let namespace;
|
|
244
|
-
/** @type {unknown} */
|
|
245
|
-
let key;
|
|
246
|
-
if (typeof ref === 'string') {
|
|
247
|
-
const dot = ref.indexOf('.');
|
|
248
|
-
namespace = dot > 0 ? ref.slice(0, dot) : '';
|
|
249
|
-
key = ref.slice(dot + 1);
|
|
250
|
-
} else if (typeof ref === 'object' && ref !== null) {
|
|
251
|
-
namespace = Reflect.get(ref, 'namespace');
|
|
252
|
-
key = Reflect.get(ref, 'key');
|
|
253
|
-
}
|
|
254
|
-
if (typeof namespace !== 'string' || typeof key !== 'string') return null;
|
|
255
|
-
return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* @param {unknown} value
|
|
260
|
-
* @returns {MetafieldRef[]} The refs that name a valid identifier, as written; each other one is skipped with a warning.
|
|
261
|
-
*/
|
|
262
|
-
function metafieldsOption(value) {
|
|
263
|
-
if (!Array.isArray(value)) return invalidOption('metafields', value, []);
|
|
264
|
-
/** @type {MetafieldRef[]} */
|
|
265
|
-
const refs = [];
|
|
266
|
-
for (const ref of /** @type {unknown[]} */ (value)) {
|
|
267
|
-
const identifier = toMetafieldIdentifier(ref);
|
|
268
|
-
if (identifier) refs.push(typeof ref === 'string' ? ref : identifier);
|
|
269
|
-
else invalidOption('metafield', ref, null);
|
|
270
|
-
}
|
|
271
|
-
return refs;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* The configuration `options` describe: defaults filled in, counts, prefix, choices and metafields validated (an
|
|
276
|
-
* invalid value warns and falls back to its default), hooks that aren't functions dropped so callers only check for
|
|
277
|
-
* null.
|
|
278
|
-
*
|
|
279
|
-
* @param {TetherOptions} options
|
|
280
|
-
* @returns {TetherConfig}
|
|
281
|
-
*/
|
|
282
|
-
export function resolveConfig(options) {
|
|
283
|
-
const domain = domainOption(options.domain);
|
|
284
|
-
const prefix =
|
|
285
|
-
typeof options.prefix === 'string' && PREFIX.test(options.prefix)
|
|
286
|
-
? options.prefix
|
|
287
|
-
: invalidOption('prefix', options.prefix, DEFAULTS.prefix);
|
|
288
|
-
/** @type {TetherConfig} */
|
|
289
|
-
const resolved = {
|
|
290
|
-
...DEFAULTS,
|
|
291
|
-
...options,
|
|
292
|
-
domain,
|
|
293
|
-
prefix,
|
|
294
|
-
storageKey: options.storageKey || `${prefix}:cart:${domain}`,
|
|
295
|
-
metafields: metafieldsOption(options.metafields ?? []),
|
|
296
|
-
checkoutTarget: choiceOption('checkoutTarget', options.checkoutTarget, ['_self', '_blank']),
|
|
297
|
-
defaultPurchase: choiceOption('defaultPurchase', options.defaultPurchase, ['one-time', 'subscription']),
|
|
298
|
-
};
|
|
299
|
-
for (const name of COUNT_OPTIONS) resolved[name] = countOption(name, options[name]);
|
|
300
|
-
for (const name of HOOK_OPTIONS) if (typeof resolved[name] !== 'function') resolved[name] = null;
|
|
301
|
-
if (resolved.apiVersion === 'unstable') {
|
|
302
|
-
warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
|
|
303
|
-
}
|
|
304
|
-
return resolved;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Replaces the shared configuration with `options` over the defaults (see `resolveConfig`).
|
|
309
|
-
*
|
|
310
|
-
* @param {TetherOptions} [options]
|
|
311
|
-
* @returns {TetherConfig} The shared `cfg`.
|
|
312
|
-
*/
|
|
313
|
-
export function configure(options = {}) {
|
|
314
|
-
const resolved = resolveConfig(options);
|
|
315
|
-
for (const key of Object.keys(cfg)) {
|
|
316
|
-
if (!(key in resolved)) Reflect.deleteProperty(cfg, key);
|
|
317
|
-
}
|
|
318
|
-
return Object.assign(cfg, resolved);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** @returns {boolean} True once a domain and a token are set. */
|
|
322
|
-
export const isConfigured = () => Boolean(cfg.domain && cfg.token);
|
|
323
|
-
|
|
324
|
-
/** @param {...unknown} args */
|
|
325
|
-
export function log(...args) {
|
|
326
|
-
if (cfg.debug) console.log(`[${cfg.prefix}]`, ...args);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** @param {...unknown} args */
|
|
330
|
-
export function warn(...args) {
|
|
331
|
-
console.warn(`[${cfg.prefix}]`, ...args);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Logs a failure that reaches the visitor and hands it to `onError`. A throwing `onError` is logged, never rethrown:
|
|
336
|
-
* the site's hook must not break the render that reported the failure.
|
|
337
|
-
*
|
|
338
|
-
* @param {unknown} error
|
|
339
|
-
* @param {string} context What failed, e.g. `product coffee-box`.
|
|
340
|
-
*/
|
|
341
|
-
export function reportError(error, context) {
|
|
342
|
-
warn(context, error instanceof Error && error.message ? error.message : error);
|
|
343
|
-
if (!cfg.onError) return;
|
|
344
|
-
try {
|
|
345
|
-
cfg.onError(error, context);
|
|
346
|
-
} catch (hookError) {
|
|
347
|
-
warn('onError threw', hookError);
|
|
348
|
-
}
|
|
349
|
-
}
|
package/src/dom.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The browser modules' DOM toolkit: what Shopify values look like on the page (money, dates, text, images, lists)
|
|
3
|
-
* and how elements show, hide and report layout changes. Attribute names go through `attr()` so the prefix is
|
|
4
|
-
* configurable. The optional parts import all of it from here (`window.Tether.__core['dom.js']`).
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, reportError, warn } from './config.js';
|
|
7
|
-
import { attr, emit, get } from './markup.js';
|
|
8
|
-
import { storeHtmlFragment } from './store-html.js';
|
|
9
|
-
import { formatMoney as formatMoneyIn, productUrl as productUrlFor } from './shopify-values.js';
|
|
10
|
-
/** @import { Money } from './shopify-values.js' */
|
|
11
|
-
|
|
12
|
-
export { attr, sel, get, has, qsa, scoped, emit } from './markup.js';
|
|
13
|
-
export { imageUrl, cdnUrl, sameImage, setImage } from './images.js';
|
|
14
|
-
export { hasTemplate, repeat, repeatKeyed } from './repeaters.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* State Tether keeps on elements, as `data-tether-*` attributes whatever the prefix: sites style and test against
|
|
18
|
-
* these names, so they stay fixed where the `tether-*` markup attributes follow `prefix`.
|
|
19
|
-
*/
|
|
20
|
-
const SEEN = 'data-tether-seen';
|
|
21
|
-
const SHOWN = 'data-tether-shown';
|
|
22
|
-
const PREVIOUS_DISPLAY = 'data-tether-prev-display';
|
|
23
|
-
|
|
24
|
-
/** Async renders change the page height in bursts; one notification per burst. */
|
|
25
|
-
const LAYOUT_DEBOUNCE_MS = 120;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The product page of a product (`productPath`).
|
|
29
|
-
*
|
|
30
|
-
* @param {string | null | undefined} handle
|
|
31
|
-
* @param {string | null} [id]
|
|
32
|
-
*/
|
|
33
|
-
export function productUrl(handle, id) {
|
|
34
|
-
return productUrlFor(cfg.productPath || '/product', handle, id);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* `Intl` throws a RangeError for a locale or currency it doesn't know; the caller then shows a plain fallback.
|
|
39
|
-
*
|
|
40
|
-
* @template T
|
|
41
|
-
* @param {() => T} format
|
|
42
|
-
* @param {T} fallback
|
|
43
|
-
* @returns {T}
|
|
44
|
-
*/
|
|
45
|
-
function formatOr(format, fallback) {
|
|
46
|
-
try {
|
|
47
|
-
return format();
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (error instanceof RangeError) return fallback;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* `30` → `30%` (en, nl) or `30 %` (de, fr), in the configured locale.
|
|
56
|
-
*
|
|
57
|
-
* @param {number | null | undefined} percentage
|
|
58
|
-
* @param {string | undefined} [locale]
|
|
59
|
-
* @returns {string} `''` for no or a zero percentage.
|
|
60
|
-
*/
|
|
61
|
-
export function formatPercent(percentage, locale = cfg.locale) {
|
|
62
|
-
if (!percentage) return '';
|
|
63
|
-
const format = () =>
|
|
64
|
-
new Intl.NumberFormat(locale || undefined, { style: 'percent', maximumFractionDigits: 0 }).format(percentage / 100);
|
|
65
|
-
return formatOr(format, `${percentage}%`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Money in the configured locale, through the site's `formatMoney` when it has one.
|
|
70
|
-
*
|
|
71
|
-
* @param {Money | null | undefined} money
|
|
72
|
-
*/
|
|
73
|
-
export function formatMoney(money) {
|
|
74
|
-
if (!money) return '';
|
|
75
|
-
if (cfg.formatMoney) return cfg.formatMoney(money, cfg.locale);
|
|
76
|
-
return formatMoneyIn(money, cfg.locale);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @param {string | null | undefined} iso
|
|
81
|
-
* @returns {string} The date in the configured locale, `''` without one.
|
|
82
|
-
*/
|
|
83
|
-
export function formatDate(iso) {
|
|
84
|
-
if (!iso) return '';
|
|
85
|
-
return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || undefined), '');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Writes HTML from the Shopify admin (product and collection descriptions) into `el`, with script removed
|
|
90
|
-
* (`store-html.js`). Everything else from Shopify is written as text (`setText`).
|
|
91
|
-
*
|
|
92
|
-
* @param {Element} el
|
|
93
|
-
* @param {string | null | undefined} html
|
|
94
|
-
*/
|
|
95
|
-
export function setStoreHtml(el, html) {
|
|
96
|
-
el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {Element} el
|
|
101
|
-
* @returns {el is HTMLInputElement | HTMLTextAreaElement}
|
|
102
|
-
*/
|
|
103
|
-
const isTextField = (el) => el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* @param {Element} el
|
|
107
|
-
* @returns {el is HTMLInputElement}
|
|
108
|
-
*/
|
|
109
|
-
const isInput = (el) => el.tagName === 'INPUT';
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Sets the text of `el`, or its value when it is a form field.
|
|
113
|
-
*
|
|
114
|
-
* @param {Element} el
|
|
115
|
-
* @param {string | number | boolean | null | undefined} value `null` and `undefined` clear it.
|
|
116
|
-
*/
|
|
117
|
-
export function setText(el, value) {
|
|
118
|
-
const text = value == null ? '' : String(value);
|
|
119
|
-
if (isTextField(el)) el.value = text;
|
|
120
|
-
else el.textContent = text;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** @param {Element} el */
|
|
124
|
-
const readText = (el) => (isTextField(el) ? el.value : el.textContent);
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* `setText` that hands a change after the first render to `onValueChange`, for scramble and roll effects.
|
|
128
|
-
*
|
|
129
|
-
* @param {HTMLElement} el
|
|
130
|
-
* @param {string | number | boolean | null | undefined} value
|
|
131
|
-
*/
|
|
132
|
-
export function setTextTracked(el, value) {
|
|
133
|
-
const before = readText(el);
|
|
134
|
-
setText(el, value);
|
|
135
|
-
if (before !== readText(el) && el.hasAttribute(SEEN) && cfg.onValueChange) {
|
|
136
|
-
try {
|
|
137
|
-
cfg.onValueChange(el, before);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
reportError(error, 'onValueChange');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
el.setAttribute(SEEN, '1');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Shows an element hidden before the script ran (a class, an inline `display: none`, a `hidden` attribute), as
|
|
147
|
-
* `block` or the value of `tether-display`.
|
|
148
|
-
*
|
|
149
|
-
* @param {HTMLElement} el
|
|
150
|
-
*/
|
|
151
|
-
function reveal(el) {
|
|
152
|
-
if (el.hasAttribute(SHOWN)) return;
|
|
153
|
-
el.setAttribute(SHOWN, '');
|
|
154
|
-
el.removeAttribute('hidden');
|
|
155
|
-
const previous = el.getAttribute(PREVIOUS_DISPLAY);
|
|
156
|
-
if (previous !== null) {
|
|
157
|
-
el.style.display = previous;
|
|
158
|
-
el.removeAttribute(PREVIOUS_DISPLAY);
|
|
159
|
-
} else if (el.style.display === 'none') {
|
|
160
|
-
el.style.display = '';
|
|
161
|
-
}
|
|
162
|
-
if (typeof getComputedStyle === 'function' && getComputedStyle(el).display === 'none') {
|
|
163
|
-
el.style.display = el.getAttribute(attr('display')) || 'block';
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Shows or hides `el`, restoring its own display value when shown again.
|
|
169
|
-
*
|
|
170
|
-
* @param {HTMLElement} el
|
|
171
|
-
* @param {boolean} visible
|
|
172
|
-
*/
|
|
173
|
-
export function show(el, visible) {
|
|
174
|
-
if (visible) {
|
|
175
|
-
reveal(el);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
el.removeAttribute(SHOWN);
|
|
179
|
-
// The display value to restore is a real one, never 'none'.
|
|
180
|
-
if (!el.hasAttribute(PREVIOUS_DISPLAY) && el.style.display !== 'none') {
|
|
181
|
-
el.setAttribute(PREVIOUS_DISPLAY, el.style.display);
|
|
182
|
-
}
|
|
183
|
-
el.style.display = 'none';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Marks an option button or input as chosen: `is-selected`, `aria-pressed`, and its own or inner radio/checkbox.
|
|
188
|
-
*
|
|
189
|
-
* @param {Element} el
|
|
190
|
-
* @param {boolean} selected
|
|
191
|
-
*/
|
|
192
|
-
export function setSelected(el, selected) {
|
|
193
|
-
el.classList.toggle('is-selected', selected);
|
|
194
|
-
el.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
195
|
-
if (isInput(el) && (el.type === 'radio' || el.type === 'checkbox')) el.checked = selected;
|
|
196
|
-
const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
|
|
197
|
-
if (input && isInput(input)) input.checked = selected;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
201
|
-
let layoutTimer;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Async renders change the page height after the site's page-transition hooks ran. After a burst of them this calls
|
|
205
|
-
* `onLayoutChange` (else refreshes ScrollTrigger) and emits `tether:layout-changed`.
|
|
206
|
-
*
|
|
207
|
-
* @param {string} reason
|
|
208
|
-
*/
|
|
209
|
-
export function notifyLayoutChanged(reason) {
|
|
210
|
-
clearTimeout(layoutTimer);
|
|
211
|
-
layoutTimer = setTimeout(() => {
|
|
212
|
-
// GSAP's ScrollTrigger, when the site has it, measures positions that a height change makes stale.
|
|
213
|
-
const { ScrollTrigger } = /** @type {Window & { ScrollTrigger?: { refresh?: () => void } }} */ (window);
|
|
214
|
-
if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
|
|
215
|
-
else ScrollTrigger?.refresh?.();
|
|
216
|
-
emit('layout-changed', { reason });
|
|
217
|
-
}, LAYOUT_DEBOUNCE_MS);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Notifies a layout change (`<reason>:images`) once every image in `root` that is still loading has loaded or failed.
|
|
222
|
-
*
|
|
223
|
-
* @param {ParentNode} root
|
|
224
|
-
* @param {string} reason
|
|
225
|
-
*/
|
|
226
|
-
export function watchImages(root, reason) {
|
|
227
|
-
const loading = [...root.querySelectorAll('img')].filter((img) => !img.complete);
|
|
228
|
-
let left = loading.length;
|
|
229
|
-
const done = () => {
|
|
230
|
-
left -= 1;
|
|
231
|
-
if (left === 0) notifyLayoutChanged(`${reason}:images`);
|
|
232
|
-
};
|
|
233
|
-
loading.forEach((img) => {
|
|
234
|
-
img.addEventListener('load', done, { once: true });
|
|
235
|
-
img.addEventListener('error', done, { once: true });
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Runs `task` with `aria-busy` on `el`; does nothing while `el` is already busy.
|
|
241
|
-
*
|
|
242
|
-
* @template T
|
|
243
|
-
* @param {Element} el
|
|
244
|
-
* @param {() => T | Promise<T>} task
|
|
245
|
-
* @returns {Promise<T | undefined>} The task's result, `undefined` when it didn't run.
|
|
246
|
-
*/
|
|
247
|
-
export function busy(el, task) {
|
|
248
|
-
if (el.getAttribute('aria-busy') === 'true') return Promise.resolve(undefined);
|
|
249
|
-
el.setAttribute('aria-busy', 'true');
|
|
250
|
-
return Promise.resolve()
|
|
251
|
-
.then(task)
|
|
252
|
-
.finally(() => {
|
|
253
|
-
el.removeAttribute('aria-busy');
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* @param {Element | null | undefined} el
|
|
259
|
-
* @param {string} name
|
|
260
|
-
* @returns {unknown} The attribute's JSON, `null` when it is missing or not JSON (with a warning).
|
|
261
|
-
*/
|
|
262
|
-
export function parseJSONAttr(el, name) {
|
|
263
|
-
const raw = el ? get(el, name) : null;
|
|
264
|
-
if (!raw) return null;
|
|
265
|
-
try {
|
|
266
|
-
return /** @type {unknown} */ (JSON.parse(raw));
|
|
267
|
-
} catch {
|
|
268
|
-
warn(`invalid JSON in ${attr(name)}`, el);
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** @typedef {string | number | boolean | null | undefined} AttributeValue */
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* `{ a: 1 }` or `[{ key, value }]` → `[{ key, value }]` as Shopify wants cart and line attributes.
|
|
277
|
-
*
|
|
278
|
-
* @param {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[] | null | undefined} attributes
|
|
279
|
-
* @returns {{ key: string, value: string }[]}
|
|
280
|
-
*/
|
|
281
|
-
export function toAttributes(attributes) {
|
|
282
|
-
if (!attributes) return [];
|
|
283
|
-
if (Array.isArray(attributes)) {
|
|
284
|
-
return attributes.map((attribute) => ({ key: String(attribute.key), value: String(attribute.value ?? '') }));
|
|
285
|
-
}
|
|
286
|
-
return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? '') }));
|
|
287
|
-
}
|