@occluze/tether 0.7.0 → 0.7.1

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.
Files changed (132) hide show
  1. package/README.md +21 -21
  2. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-CXMA5RPZ.js} +1 -1
  3. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IZO7FEOZ.js} +1 -1
  4. package/dist/esm/chunks/{analytics-2V632IRV.js → analytics-PG5FPRDN.js} +1 -1
  5. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-4HD4J5JC.js} +1 -1
  6. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-6TFMLT4A.js} +2 -2
  7. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-BBVUVPMO.js} +1 -1
  8. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  9. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  10. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  11. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-N3V45JPS.js} +1 -1
  12. package/dist/esm/chunks/{chunk-YLUULATY.js → chunk-RPMBCAGS.js} +1 -1
  13. package/dist/esm/chunks/{chunk-MPDWM2NN.js → chunk-VFNHIGCS.js} +1 -1
  14. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-WT7RGGJU.js} +1 -1
  15. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  16. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  17. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-XCEBQKBO.js} +1 -1
  18. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  19. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-M4IQ62CT.js} +1 -1
  20. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/server.js +2 -2
  23. package/dist/tether.account-pages.js +13 -3
  24. package/dist/tether.account-pages.min.js +1 -1
  25. package/dist/tether.account.js +13 -3
  26. package/dist/tether.account.min.js +1 -1
  27. package/dist/tether.analytics.js +18 -3
  28. package/dist/tether.analytics.min.js +1 -1
  29. package/dist/tether.diagnose.js +18 -3
  30. package/dist/tether.diagnose.min.js +2 -2
  31. package/dist/tether.extras.js +19 -6
  32. package/dist/tether.extras.min.js +1 -1
  33. package/dist/tether.js +162 -112
  34. package/dist/tether.markets.js +18 -3
  35. package/dist/tether.markets.min.js +1 -1
  36. package/dist/tether.min.js +3 -3
  37. package/dist/tether.min.js.map +4 -4
  38. package/dist/tether.search.js +18 -3
  39. package/dist/tether.search.min.js +1 -1
  40. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  41. package/dist/types/src/cart/mutations.d.ts +6 -0
  42. package/dist/types/src/cart/store.d.ts +5 -2
  43. package/dist/types/src/cart.d.ts +2 -2
  44. package/dist/types/src/client.d.ts +53 -19
  45. package/dist/types/src/config.d.ts +6 -1
  46. package/dist/types/src/index.d.ts +2 -2
  47. package/dist/types/src/product-shape.d.ts +3 -1
  48. package/package.json +1 -2
  49. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  50. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  51. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  52. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  53. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  54. package/dist/esm/chunks/search-UJ73YBQ5.js +0 -1
  55. package/src/account/addresses-view.ts +0 -58
  56. package/src/account/answers.ts +0 -79
  57. package/src/account/markup.ts +0 -36
  58. package/src/account/operations.ts +0 -145
  59. package/src/account/order-lines.ts +0 -75
  60. package/src/account/order-list.ts +0 -107
  61. package/src/account/order-page.ts +0 -137
  62. package/src/account/page-answers.ts +0 -317
  63. package/src/account/page-events.ts +0 -253
  64. package/src/account/server-client.ts +0 -183
  65. package/src/account/subscriptions-view.ts +0 -115
  66. package/src/account-api.ts +0 -98
  67. package/src/account-marks.ts +0 -35
  68. package/src/account-pages.ts +0 -81
  69. package/src/account.ts +0 -284
  70. package/src/amounts.ts +0 -64
  71. package/src/analytics-shopify.ts +0 -246
  72. package/src/analytics.ts +0 -315
  73. package/src/api-version.ts +0 -79
  74. package/src/astro.ts +0 -272
  75. package/src/cart/cart-shape.ts +0 -25
  76. package/src/cart/checkout.ts +0 -101
  77. package/src/cart/drawer.ts +0 -125
  78. package/src/cart/mutations.ts +0 -237
  79. package/src/cart/render.ts +0 -266
  80. package/src/cart/store.ts +0 -279
  81. package/src/cart.ts +0 -345
  82. package/src/client.ts +0 -391
  83. package/src/collection.ts +0 -175
  84. package/src/config-sections.ts +0 -284
  85. package/src/config.ts +0 -480
  86. package/src/controls/cart-controls.ts +0 -123
  87. package/src/controls/discount.ts +0 -75
  88. package/src/controls/extras.ts +0 -54
  89. package/src/controls/navigation.ts +0 -77
  90. package/src/diagnose-markup.ts +0 -298
  91. package/src/diagnose.ts +0 -294
  92. package/src/dom.ts +0 -268
  93. package/src/extras.ts +0 -136
  94. package/src/fragments.ts +0 -261
  95. package/src/globals.d.ts +0 -15
  96. package/src/images.ts +0 -91
  97. package/src/index.ts +0 -415
  98. package/src/initial.ts +0 -101
  99. package/src/lazy-iife.ts +0 -197
  100. package/src/lazy.ts +0 -42
  101. package/src/loader.ts +0 -363
  102. package/src/market-codes.ts +0 -6
  103. package/src/markets-ui.ts +0 -227
  104. package/src/markets.ts +0 -246
  105. package/src/markup.ts +0 -127
  106. package/src/mounting.ts +0 -264
  107. package/src/page-market.ts +0 -110
  108. package/src/part-integrity.ts +0 -7
  109. package/src/parts/account-pages.ts +0 -7
  110. package/src/parts/account.ts +0 -7
  111. package/src/parts/analytics.ts +0 -7
  112. package/src/parts/diagnose.ts +0 -7
  113. package/src/parts/extras.ts +0 -7
  114. package/src/parts/markets.ts +0 -7
  115. package/src/parts/search.ts +0 -7
  116. package/src/presets/barba.ts +0 -194
  117. package/src/product-events.ts +0 -115
  118. package/src/product-render.ts +0 -218
  119. package/src/product-shape.ts +0 -49
  120. package/src/product-state.ts +0 -318
  121. package/src/product.ts +0 -308
  122. package/src/repeaters.ts +0 -161
  123. package/src/search.ts +0 -305
  124. package/src/selling-plans.ts +0 -145
  125. package/src/server.ts +0 -211
  126. package/src/shopify-values.ts +0 -91
  127. package/src/status.ts +0 -106
  128. package/src/storage.ts +0 -34
  129. package/src/store-html.ts +0 -359
  130. package/src/storefront/catalog.ts +0 -196
  131. package/src/storefront/json-ld.ts +0 -144
  132. package/src/variant-render.ts +0 -139
package/src/mounting.ts DELETED
@@ -1,264 +0,0 @@
1
- /**
2
- * The product scopes and lists on the page: mounting a product root, the live roots a country switch re-renders, and
3
- * the products the server rendered into the page. Shared by `index.ts` and the optional markets part.
4
- */
5
- import { cfg, log, warn, reportError } from './config.ts';
6
- import { ProductScope, scopes, rememberOptionNames } from './product.ts';
7
- import { listSelector } from './collection.ts';
8
- import { getProduct, takeStale, refreshProduct, hydrateProducts } from './loader.ts';
9
- import { resolveProductKey, setScopeStatus, isNotFound } from './status.ts';
10
- import { attr, get, qsa, emit } from './dom.ts';
11
- import { toCountry, toLanguage } from './market-codes.ts';
12
- import { isRecord } from '../server/src/shared/json.ts';
13
- import { isProductShape } from './product-shape.ts';
14
- import type { Product } from './fragments.ts';
15
- import type { CountryCode, LanguageCode } from './market-codes.ts';
16
-
17
- /** A mounted `ListScope`. */
18
- interface MountedList {
19
- root: Element;
20
- load: (more?: boolean) => Promise<unknown>;
21
- }
22
-
23
- type KeySource = Pick<ReturnType<typeof resolveProductKey>, 'fromUrl' | 'raw'>;
24
-
25
- /**
26
- * The marker on the `<script type="application/json">` tags `tether/server`'s `hydrate()` writes. It is fixed, not
27
- * `<prefix>-hydrate`, because the server writes it without knowing the browser's prefix.
28
- */
29
- const HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
30
-
31
- export const mountedProducts = new WeakSet<Element>();
32
- export const mountedLists = new WeakSet<Element>();
33
- /** Product roots to re-render after a country switch; pruned once disconnected. */
34
- export const liveProducts = new Set<HTMLElement>();
35
- /** Lists to reload after a country switch; pruned once disconnected. */
36
- export const liveLists = new Set<MountedList>();
37
- /** Each product root's latest request, so a slow older response can't win. */
38
- const latestRequest = new WeakMap<Element, object>();
39
-
40
- /** Forgets roots a page transition removed, so they aren't kept alive. */
41
- export function pruneLive(): void {
42
- liveProducts.forEach((el) => {
43
- if (!el.isConnected) liveProducts.delete(el);
44
- });
45
- liveLists.forEach((list) => {
46
- if (!list.root.isConnected) liveLists.delete(list);
47
- });
48
- }
49
-
50
- /** @returns `root` itself when it matches, then the matches inside it. */
51
- export function within(root: ParentNode & Partial<Pick<Element, 'matches'>>, selector: string): HTMLElement[] {
52
- // A root that matches carries Tether's markup, which goes on HTML elements (see `qsa`).
53
- const self = root.matches?.(selector) ? [root as HTMLElement] : [];
54
- return [...self, ...qsa(root, selector)];
55
- }
56
-
57
- /** `tether-page-title="{title} | My shop"` on a product root sets `document.title`, for dynamic product pages. */
58
- function applyPageTitle(el: Element, product: Product): void {
59
- const template = get(el, 'page-title');
60
- if (!template || typeof document === 'undefined') return;
61
- document.title = template
62
- .replace(/\{title\}/g, product.title)
63
- .replace(/\{vendor\}/g, product.vendor || '')
64
- .replace(/\{type\}/g, product.productType || '');
65
- }
66
-
67
- /**
68
- * A product root without a key: on a dynamic page opened without `?handle=` (or with an empty path) it is a product
69
- * that isn't there; inside a list it gets its handle when the list renders.
70
- */
71
- function mountWithoutKey(el: HTMLElement, { fromUrl, raw }: KeySource): void {
72
- if (fromUrl) {
73
- mountedProducts.add(el);
74
- setScopeStatus(el, 'not-found');
75
- emit('product-not-found', { key: null, source: raw }, el);
76
- } else if (!el.closest(listSelector())) {
77
- warn(`[${attr('product')}] without a value. Bind it to the Shopify handle field, or use "url:handle".`, el);
78
- }
79
- }
80
-
81
- function showLoadFailure(el: HTMLElement, key: string, { fromUrl, raw }: KeySource, error: unknown): void {
82
- const notFound = isNotFound(error);
83
- setScopeStatus(el, notFound ? 'not-found' : 'error', error);
84
- if (!notFound) {
85
- reportError(error, `product ${key}`);
86
- emit('error', { error, context: 'product', key }, el);
87
- return;
88
- }
89
- emit('product-not-found', { key, source: raw }, el);
90
- // A typo in a URL isn't worth an alert; a CMS item pointing at a deleted product is.
91
- if (fromUrl) log(`product not found: ${key}`);
92
- else reportError(error, `product ${key}`);
93
- }
94
-
95
- /**
96
- * Mounts one product root.
97
- *
98
- * @returns True when a product request was started.
99
- */
100
- export function mountProduct(el: HTMLElement): boolean {
101
- if (mountedProducts.has(el)) return false;
102
- const { key, ...source } = resolveProductKey(el);
103
- if (!key) {
104
- mountWithoutKey(el, source);
105
- return false;
106
- }
107
- mountedProducts.add(el);
108
- liveProducts.add(el);
109
- const request = {};
110
- latestRequest.set(el, request);
111
- const isLatest = () => latestRequest.get(el) === request;
112
- // A rendered scope keeps showing its content while it refreshes (a country switch).
113
- if (scopes.get(el)) el.setAttribute(attr('state'), 'updating');
114
- else setScopeStatus(el, 'loading');
115
- const loading = trackMarket(getProduct(key));
116
- loading
117
- .then((product) => {
118
- if (!isLatest()) return;
119
- const existing = scopes.get(el);
120
- if (existing) existing.update(product);
121
- else new ProductScope(el, product);
122
- applyPageTitle(el, product);
123
- if (cfg.revalidate && takeStale(key)) queueRefresh(key);
124
- })
125
- .catch((error: unknown) => {
126
- if (isLatest()) showLoadFailure(el, key, source, error);
127
- });
128
- return true;
129
- }
130
-
131
- /** Loads again the product scopes that failed on a network or API error, for when the browser is back online. */
132
- export function retryFailed(): void {
133
- pruneLive();
134
- liveProducts.forEach((el) => {
135
- if (el.getAttribute(attr('state')) !== 'error' || el.getAttribute(attr('error')) === 'not-found') return;
136
- mountedProducts.delete(el);
137
- mountProduct(el);
138
- });
139
- }
140
-
141
- /** Server-rendered products to refresh in the next batch. */
142
- let refreshQueue = new Set<string>();
143
-
144
- function queueRefresh(key: string): void {
145
- if (!refreshQueue.size) setTimeout(flushRefresh, 0);
146
- refreshQueue.add(key);
147
- }
148
-
149
- /** @param key The key the refresh was asked for. */
150
- function applyRefresh(key: string, fresh: Product): void {
151
- liveProducts.forEach((el) => {
152
- const scope = scopes.get(el);
153
- if (!scope || !el.isConnected) return;
154
- const rootKey = resolveProductKey(el).key;
155
- if (rootKey !== key && rootKey !== fresh.handle && rootKey !== fresh.id) return;
156
- if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
157
- });
158
- }
159
-
160
- /** One batched background request refreshes every server-rendered product queued. */
161
- function flushRefresh() {
162
- const keys = [...refreshQueue];
163
- refreshQueue = new Set();
164
- keys.forEach((key) => {
165
- refreshProduct(key)
166
- .then((fresh) => {
167
- applyRefresh(key, fresh);
168
- })
169
- .catch((error: unknown) => {
170
- reportError(error, `refresh ${key}`);
171
- });
172
- });
173
- }
174
-
175
- /**
176
- * While the page's server-rendered prices are another market's: the loads to wait for before
177
- * `html.tether-market-pending` goes. `null` when no market is pending.
178
- */
179
- let marketPending: Set<Promise<unknown>> | null = null;
180
-
181
- /**
182
- * Lists, and anything else that loads prices, hold `html.tether-market-pending` until they are in.
183
- *
184
- * @returns `loading` itself.
185
- */
186
- export const trackMarket = <T>(loading: Promise<T>): Promise<T> => {
187
- marketPending?.add(loading);
188
- return loading;
189
- };
190
-
191
- /** After a mount: drops `html.tether-market-pending` once this page's products are live. */
192
- export function settleMarket(): void {
193
- if (!marketPending) return;
194
- const waits = [...marketPending];
195
- void Promise.allSettled(waits).then(() => {
196
- if (!marketPending || waits.some((wait) => !marketPending?.has(wait))) return;
197
- if (waits.length !== marketPending.size) {
198
- settleMarket(); // more loads came in meanwhile
199
- return;
200
- }
201
- marketPending = null;
202
- document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
203
- });
204
- }
205
-
206
- /** What `tether/server`'s `hydrate()` writes: the market it fetched in, and the products. */
207
- interface HydratePayload {
208
- country: CountryCode | undefined;
209
- language: LanguageCode | undefined;
210
- products: Product[];
211
- }
212
-
213
- /** @param json A hydrate tag's JSON. */
214
- function toHydratePayload(json: unknown): HydratePayload {
215
- if (!isRecord(json)) return { country: undefined, language: undefined, products: [] };
216
- const { products } = json;
217
- const entries: unknown[] = Array.isArray(products) ? products : [];
218
- return {
219
- country: toCountry(json['country']) ?? undefined,
220
- language: toLanguage(json['language']) ?? undefined,
221
- products: entries.filter(isProductShape),
222
- };
223
- }
224
-
225
- /** Hydrate tags already read. */
226
- const readScripts = new WeakSet<Element>();
227
- /**
228
- * Uses the products in the page's hydrate tags (`tether/server` `hydrate()`) without a request. A tag made for another
229
- * market is skipped, and marks the page (`html.tether-market-pending`) until the live prices are in, so CSS can hide
230
- * prices meanwhile.
231
- *
232
- * Trust: a hydrate tag is part of the page, written by the site's own build like the rest of its HTML and scripts, so
233
- * its prices and products are shown until the background refresh replaces them. Whoever can write a tag into the page
234
- * can already write a script there; the shape check (`isProductShape`) is for a truncated or hand-written tag, which
235
- * is skipped instead of failing a render.
236
- *
237
- * @param root Where to look: the document for a mount (a tag may sit in `<head>` or anywhere else on the page), the
238
- * added elements for `observe`.
239
- */
240
- export function hydrateFromPage(root: Document | Element = document): void {
241
- within(root, HYDRATE_SELECTOR).forEach((script) => {
242
- if (readScripts.has(script)) return;
243
- readScripts.add(script);
244
- let json: unknown;
245
- try {
246
- json = JSON.parse(script.textContent);
247
- } catch (error) {
248
- warn('bad data-tether-hydrate', error);
249
- return;
250
- }
251
- const { country, language, products } = toHydratePayload(json);
252
- // The build's option names, also for other languages.
253
- rememberOptionNames(products);
254
- if (country === cfg.country && language === cfg.language) {
255
- hydrateProducts(products);
256
- return;
257
- }
258
- log('hydration skipped: other market');
259
- if (!marketPending) {
260
- marketPending = new Set();
261
- document.documentElement.classList.add(`${cfg.prefix}-market-pending`);
262
- }
263
- });
264
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * The market the page uses: a page built for one market (`<meta name="tether-page-country">`) wins, then the visitor's
3
- * saved or detected market, then the configured one. Init applies it to `cfg` once. Astro's ClientRouter keeps the
4
- * script running across pages, so each arrival asks again: the next page may be built for another market, or for none.
5
- */
6
- import { cfg, log, reportError } from './config.ts';
7
- import { loadPart } from './lazy.ts';
8
- import { toCountry, toLanguage } from './market-codes.ts';
9
- import { readSavedMarket } from './markets.ts';
10
- import type { TetherOptions } from './config.ts';
11
- import type { Market, DetectedMarket } from './markets.ts';
12
- import type { Parts } from './lazy.ts';
13
- import type { ShopifyCart } from './cart/store.ts';
14
-
15
- export type SetCountryOptions = Parameters<Parts['markets']['setCountry']>[1];
16
-
17
- /** The market of the config, before a page's or visitor's market applied. */
18
- interface ConfiguredMarket {
19
- country: string | undefined;
20
- language: string | undefined;
21
- locale: string | undefined;
22
- }
23
-
24
- let configuredMarket: ConfiguredMarket = { country: undefined, language: undefined, locale: undefined };
25
-
26
- /** Whether the page arrived at last was built for its own market, which the next page without one switches back from. */
27
- let lastPageHadOwnMarket = false;
28
-
29
- /**
30
- * `<meta name="tether-page-country" content="DE">`, with `tether-page-language` and `tether-page-locale`. Never
31
- * remembered: it belongs to the page, not to the visitor.
32
- *
33
- * @returns `null` when the page has no valid country of its own.
34
- */
35
- function pageMarket(): Market | null {
36
- const meta = (name: string) =>
37
- document.querySelector(`meta[name="${cfg.prefix}-page-${name}"]`)?.getAttribute('content') || undefined;
38
- const country = toCountry(meta('country'));
39
- if (!country) return null;
40
- return { country, language: toLanguage(meta('language')) ?? undefined, locale: meta('locale') };
41
- }
42
-
43
- /**
44
- * Without `markets.detect` only the visitor's saved choice applies, read without loading the markets part.
45
- */
46
- async function visitorMarket(): Promise<Market | DetectedMarket | null> {
47
- const markets = cfg.markets ?? {};
48
- if (markets.detect === false) return null;
49
- if (!markets.detect) return markets.remember === false ? null : readSavedMarket();
50
- try {
51
- return (await loadPart('markets')).detectMarket(markets);
52
- } catch (error) {
53
- reportError(error, 'markets');
54
- return null;
55
- }
56
- }
57
-
58
- /**
59
- * Sets `cfg`'s market for the first page, over the configured one.
60
- *
61
- * @param overrides What `init()` was called with: a `locale` there wins over the visitor's.
62
- */
63
- export async function applyStartMarket(overrides: TetherOptions): Promise<void> {
64
- configuredMarket = { country: cfg.country, language: cfg.language, locale: cfg.locale };
65
- const page = pageMarket();
66
- lastPageHadOwnMarket = page !== null;
67
- if (page) {
68
- cfg.country = page.country;
69
- cfg.language = page.language;
70
- if (page.locale) cfg.locale = page.locale;
71
- return;
72
- }
73
- const market = await visitorMarket();
74
- if (!market) return;
75
- if (market.country) cfg.country = market.country;
76
- if (market.language) cfg.language = market.language;
77
- if (market.locale && !overrides.locale) cfg.locale = market.locale;
78
- log('market', market);
79
- }
80
-
81
- const sameCode = (a: string | undefined, b: string | undefined) => (a ?? '').toUpperCase() === (b ?? '').toUpperCase();
82
-
83
- /**
84
- * The market to switch to on arriving at the current page. A page built for a market switches to it; the first page
85
- * without one after such a page switches back to the visitor's own choice, else to the configured market.
86
- *
87
- * @returns `null` when the page stays in the market in use.
88
- */
89
- export function marketForArrival(): {
90
- country: string;
91
- language: string | undefined;
92
- locale: string | undefined;
93
- } | null {
94
- const page = pageMarket();
95
- const leftOwnMarket = page === null && lastPageHadOwnMarket;
96
- lastPageHadOwnMarket = page !== null;
97
- const target = page ?? (leftOwnMarket ? (readSavedMarket() ?? configuredMarket) : null);
98
- if (!target?.country) return null;
99
- if (sameCode(target.country, cfg.country) && sameCode(target.language, cfg.language)) return null;
100
- return { country: target.country, language: target.language, locale: target.locale };
101
- }
102
-
103
- /**
104
- * `Tether.setCountry('DE')`: loads the markets part on first use.
105
- *
106
- * @returns The cart once everything is in the new market.
107
- */
108
- export function setCountry(code: string, options?: SetCountryOptions): Promise<ShopifyCart | null> {
109
- return loadPart('markets').then((part) => part.setCountry(code, options));
110
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * The Subresource Integrity hash of each optional part's file, by file name, so the core loads only the parts built
3
- * with it (src/lazy-iife.ts). Every `<script>` build, watch mode included, puts the hashes of the parts it built into
4
- * the core in place of this module (scripts/build.ts); the empty map here only lets the source type-check, and a part
5
- * missing from the map is not loaded.
6
- */
7
- export const PART_INTEGRITY: Readonly<Record<string, string>> = {};
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.account-pages.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../account-pages.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.account-pages.js runs after the Tether script, which loads it');
7
- Tether.__parts['account-pages'] = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.account.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../account.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.account.js runs after the Tether script, which loads it');
7
- Tether.__parts.account = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.analytics.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../analytics.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.analytics.js runs after the Tether script, which loads it');
7
- Tether.__parts.analytics = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.diagnose.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../diagnose.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.diagnose.js runs after the Tether script, which loads it');
7
- Tether.__parts.diagnose = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.extras.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../extras.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.extras.js runs after the Tether script, which loads it');
7
- Tether.__parts.extras = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.markets.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../markets-ui.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.markets.js runs after the Tether script, which loads it');
7
- Tether.__parts.markets = part;
@@ -1,7 +0,0 @@
1
- // The <script> build's tether.search.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
2
- import * as part from '../search.ts';
3
- import type { TetherInternals } from '../lazy.ts';
4
-
5
- const { Tether } = window as { Tether?: TetherInternals };
6
- if (!Tether) throw new Error('tether.search.js runs after the Tether script, which loads it');
7
- Tether.__parts.search = part;
@@ -1,194 +0,0 @@
1
- /**
2
- * Preset for sites with page transitions (Barba, Swup), smooth scroll (Lenis) and GSAP. Nothing is read from globals
3
- * except GSAP and ScrollTrigger by default: pass in what your site has.
4
- *
5
- * import Tether from "@occluze/tether";
6
- * import { barbaPreset } from "@occluze/tether/presets/barba";
7
- *
8
- * Tether.init({
9
- * ...window.TETHER_CONFIG,
10
- * ...barbaPreset({
11
- * lenis: () => myLenis, // resized after async renders
12
- * lockScroll: (drawer) => myLock.lock(drawer), // when the cart opens
13
- * unlockScroll: () => myLock.unlock(), // when the cart closes
14
- * onValueChange: (el, previous) => {}, // e.g. a text scramble
15
- * }),
16
- * });
17
- *
18
- * // once per page container, e.g. in Barba's beforeEnter AND once():
19
- * Tether.mount(container);
20
- * // when leaving a page:
21
- * Tether.unmount();
22
- */
23
- import type { TetherConfig } from '../config.ts';
24
-
25
- // GSAP isn't a dependency, so the two calls this preset makes are described here.
26
- type TweenVars = Record<string, string | number | (() => void)>;
27
- interface Gsap {
28
- fromTo: (target: HTMLElement, from: TweenVars, to: TweenVars) => unknown;
29
- to: (target: HTMLElement, to: TweenVars) => unknown;
30
- }
31
-
32
- /** `gsap`, `lenis` and `scrollTrigger` are getters, read each time they are used, so an instance made later is found. */
33
- interface BarbaPresetOptions {
34
- /** Default `window.gsap`. */
35
- gsap?: () => unknown;
36
- /** Resized after async renders. */
37
- lenis?: (() => unknown) | null;
38
- /** Default `window.ScrollTrigger`, refreshed after async renders. */
39
- scrollTrigger?: () => unknown;
40
- /** When the cart opens. */
41
- lockScroll?: ((drawer: HTMLElement) => void) | null;
42
- /** When the cart closes. */
43
- unlockScroll?: (() => void) | null;
44
- /** After a cart line animated in or out. */
45
- onResize?: (() => void) | null;
46
- onValueChange?: TetherConfig['onValueChange'];
47
- /** Animate cart lines in and out (default true). */
48
- animateLines?: boolean;
49
- /** Seconds. */
50
- lineDuration?: number;
51
- }
52
-
53
- /** The hooks the preset always sets, and `onValueChange` as passed (`null` when it isn't). */
54
- type BarbaPresetConfig = { autoMount: false } & {
55
- [Hook in 'onLayoutChange' | 'onCartOpen' | 'onCartClose' | 'onLineEnter' | 'onLineLeave']: NonNullable<
56
- TetherConfig[Hook]
57
- >;
58
- } & Pick<TetherConfig, 'onValueChange'>;
59
-
60
- export type { BarbaPresetOptions, BarbaPresetConfig };
61
-
62
- const LINE_DURATION_SECONDS = 0.3;
63
- /** @returns A new object each time: GSAP writes into the vars it is given. */
64
- const collapsed = (): TweenVars => ({
65
- height: 0,
66
- paddingTop: 0,
67
- paddingBottom: 0,
68
- borderBottomWidth: 0,
69
- opacity: 0,
70
- overflow: 'hidden',
71
- });
72
- const ANIMATED_PROPS = 'height,paddingTop,paddingBottom,borderBottomWidth,opacity,overflow';
73
-
74
- const reducedMotion = () =>
75
- typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
76
-
77
- const GETTER_OPTIONS = ['gsap', 'lenis', 'scrollTrigger'] as const;
78
-
79
- /**
80
- * @throws {TypeError} When a getter option is a value, which JavaScript callers can pass: a value that is a function
81
- * (ScrollTrigger is a class) can't be told from a getter.
82
- */
83
- function checkGetters(options: BarbaPresetOptions) {
84
- for (const name of GETTER_OPTIONS) {
85
- const option: unknown = options[name];
86
- if (option !== undefined && option !== null && typeof option !== 'function') {
87
- throw new TypeError(`barbaPreset: ${name} must be a function that returns it, such as () => ${name}`);
88
- }
89
- }
90
- }
91
-
92
- /**
93
- * Calls `target[method]()` when it has one: Lenis and ScrollTrigger are optional and versions differ.
94
- *
95
- * @param target An object, or a class with a static method (`ScrollTrigger.refresh()`).
96
- */
97
- function callIfPresent(target: unknown, method: string) {
98
- if ((typeof target !== 'object' && typeof target !== 'function') || target === null) return;
99
- const fn: unknown = Reflect.get(target, method);
100
- // A method of `target`, called on it.
101
- if (typeof fn === 'function') (fn as (this: unknown) => unknown).call(target);
102
- }
103
-
104
- const isGsap = (value: unknown): value is Gsap =>
105
- typeof value === 'object' &&
106
- value !== null &&
107
- typeof Reflect.get(value, 'fromTo') === 'function' &&
108
- typeof Reflect.get(value, 'to') === 'function';
109
-
110
- /**
111
- * Hooks for sites with page transitions, smooth scroll and GSAP: spread into `Tether.init()`.
112
- */
113
- export function barbaPreset(options: BarbaPresetOptions = {}): BarbaPresetConfig {
114
- checkGetters(options);
115
- const {
116
- gsap = (): unknown => Reflect.get(window, 'gsap'),
117
- lenis = null,
118
- scrollTrigger = (): unknown => Reflect.get(window, 'ScrollTrigger'),
119
- lockScroll = null,
120
- unlockScroll = null,
121
- onResize = null,
122
- onValueChange = null,
123
- animateLines = true,
124
- lineDuration = LINE_DURATION_SECONDS,
125
- } = options;
126
-
127
- /** Whether this preset holds the scroll lock, so a modal's own lock stays intact. */
128
- let locked = false;
129
-
130
- /** @returns GSAP, when this line should animate. */
131
- const animator = (node: HTMLElement): Gsap | null => {
132
- if (!animateLines || reducedMotion() || node.offsetHeight <= 0) return null;
133
- const animation = gsap();
134
- return isGsap(animation) ? animation : null;
135
- };
136
-
137
- return {
138
- autoMount: false,
139
-
140
- onLayoutChange() {
141
- callIfPresent(lenis?.(), 'resize');
142
- callIfPresent(scrollTrigger(), 'refresh');
143
- },
144
-
145
- onCartOpen(drawers) {
146
- const drawer = drawers[0];
147
- if (locked || !lockScroll || !drawer) return;
148
- lockScroll(drawer);
149
- locked = true;
150
- },
151
-
152
- onCartClose() {
153
- if (locked && unlockScroll) unlockScroll();
154
- locked = false;
155
- },
156
-
157
- onLineEnter(node) {
158
- const animation = animator(node);
159
- if (!animation) return;
160
- const style = getComputedStyle(node);
161
- animation.fromTo(node, collapsed(), {
162
- height: node.offsetHeight,
163
- paddingTop: style.paddingTop,
164
- paddingBottom: style.paddingBottom,
165
- borderBottomWidth: style.borderBottomWidth,
166
- opacity: 1,
167
- duration: lineDuration,
168
- clearProps: ANIMATED_PROPS,
169
- onComplete: () => onResize?.(),
170
- });
171
- },
172
-
173
- onLineLeave(node, done) {
174
- const animation = animator(node);
175
- if (!animation) {
176
- done();
177
- return;
178
- }
179
- animation.to(node, {
180
- ...collapsed(),
181
- duration: lineDuration,
182
- onComplete: () => {
183
- done();
184
- onResize?.();
185
- },
186
- });
187
- },
188
-
189
- onValueChange,
190
- };
191
- }
192
-
193
- /** @alias `barbaPreset`, for `import barbaPreset from '@occluze/tether/presets/barba'`. */
194
- export default barbaPreset;