@occluze/tether 0.6.7 → 0.6.8

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 (135) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
  4. package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
  5. package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
  6. package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
  7. package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
  8. package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
  9. package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
  10. package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
  11. package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
  12. package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
  13. package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
  14. package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
  15. package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
  16. package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
  17. package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
  18. package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
  19. package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
  20. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
  21. package/dist/esm/chunks/search-OU75RNQL.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -1
  24. package/dist/presets/barba.esm.js +56 -36
  25. package/dist/tether.account-pages.js +773 -442
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +648 -318
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +318 -191
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +331 -242
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +78 -33
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3176 -2015
  36. package/dist/tether.markets.js +231 -126
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +221 -126
  41. package/dist/tether.search.min.js +1 -1
  42. package/package.json +23 -4
  43. package/src/account/addresses-view.js +92 -0
  44. package/src/account/browser-session.js +359 -0
  45. package/src/account/markup.js +48 -0
  46. package/src/account/operations.js +180 -0
  47. package/src/account/order-lines.js +96 -0
  48. package/src/account/order-list.js +91 -0
  49. package/src/account/order-page.js +192 -0
  50. package/src/account/page-events.js +288 -0
  51. package/src/account/server-client.js +172 -0
  52. package/src/account/shared.js +111 -0
  53. package/src/account/shopify-auth.js +150 -0
  54. package/src/account/subscriptions-view.js +158 -0
  55. package/src/account-api.js +118 -0
  56. package/src/account-links.js +35 -0
  57. package/src/account-pages.js +69 -298
  58. package/src/account.js +280 -323
  59. package/src/amounts.js +36 -0
  60. package/src/analytics-shopify.js +241 -0
  61. package/src/analytics.js +304 -184
  62. package/src/api-version.js +75 -25
  63. package/src/astro.js +233 -68
  64. package/src/cart/checkout.js +97 -0
  65. package/src/cart/drawer.js +138 -0
  66. package/src/cart/mutations.js +234 -0
  67. package/src/cart/render.js +294 -0
  68. package/src/cart/store.js +326 -0
  69. package/src/cart.js +320 -488
  70. package/src/client.js +214 -78
  71. package/src/collection.js +126 -57
  72. package/src/config.js +323 -59
  73. package/src/controls/cart-controls.js +155 -0
  74. package/src/controls/discount.js +96 -0
  75. package/src/controls/extras.js +63 -0
  76. package/src/controls/navigation.js +83 -0
  77. package/src/diagnose-markup.js +333 -0
  78. package/src/diagnose.js +251 -202
  79. package/src/dom.js +219 -220
  80. package/src/extras.js +119 -37
  81. package/src/fragments.js +115 -22
  82. package/src/images.js +104 -0
  83. package/src/index.js +245 -287
  84. package/src/initial.js +78 -66
  85. package/src/lazy-iife.js +92 -22
  86. package/src/lazy.js +25 -4
  87. package/src/loader.js +263 -124
  88. package/src/market-codes.js +35 -0
  89. package/src/markets-ui.js +169 -55
  90. package/src/markets.js +286 -85
  91. package/src/markup.js +64 -0
  92. package/src/mounting.js +214 -91
  93. package/src/page-market.js +116 -0
  94. package/src/parts/account-pages.js +4 -1
  95. package/src/parts/account.js +4 -1
  96. package/src/parts/analytics.js +4 -1
  97. package/src/parts/diagnose.js +4 -1
  98. package/src/parts/extras.js +4 -1
  99. package/src/parts/markets.js +4 -1
  100. package/src/parts/search.js +4 -1
  101. package/src/presets/barba.js +120 -30
  102. package/src/product-events.js +130 -0
  103. package/src/product-render.js +257 -0
  104. package/src/product-state.js +318 -0
  105. package/src/product.js +233 -459
  106. package/src/repeaters.js +173 -0
  107. package/src/search.js +277 -107
  108. package/src/selling-plans.js +107 -47
  109. package/src/server.js +168 -205
  110. package/src/shopify-values.js +89 -0
  111. package/src/status.js +99 -39
  112. package/src/store-html.js +0 -0
  113. package/src/storefront/catalog.js +199 -0
  114. package/src/storefront/json-ld.js +167 -0
  115. package/src/variant-render.js +165 -0
  116. package/types/astro.d.ts +13 -2
  117. package/types/index.d.ts +178 -27
  118. package/types/server.d.ts +89 -17
  119. package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
  120. package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
  121. package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
  122. package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
  123. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  124. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  125. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  126. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  127. package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
  128. package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
  129. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  130. package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
  131. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  132. package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
  133. package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
  134. package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
  135. package/dist/esm/chunks/search-NILMGYXG.js +0 -1
@@ -0,0 +1,326 @@
1
+ /**
2
+ * The cart on this page: what Shopify last said about it (`CartStatus`), the quantities quick clicks asked for, the
3
+ * customer it belongs to, and the one queue every change goes through. A change of what the page shows goes through
4
+ * here, so the document (`renderCart`) and the listeners (`tether:cart-updated`) always see the store's cart.
5
+ */
6
+ import { cfg } from '../config.js';
7
+ import { emit } from '../dom.js';
8
+ import { isOpen } from './drawer.js';
9
+ import { renderCart, quantityOf, showSavedCount } from './render.js';
10
+ /** @import { CartWarning } from '../config.js' */
11
+ /** @import { Image } from '../fragments.js' */
12
+ /** @import { Money } from '../shopify-values.js' */
13
+
14
+ /** @typedef {{ key: string, value: string }} Attribute */
15
+ /**
16
+ * @typedef {object} CartVariant A line's `merchandise`: Shopify's `Merchandise` union has only `ProductVariant`.
17
+ * @property {string} id
18
+ * @property {string} title
19
+ * @property {string | null} sku
20
+ * @property {boolean} availableForSale
21
+ * @property {boolean} requiresShipping
22
+ * @property {number | null} [quantityAvailable] Absent when the store's token can't read inventory.
23
+ * @property {Image | null} image
24
+ * @property {Money} price
25
+ * @property {Money | null} compareAtPrice
26
+ * @property {{ name: string, value: string }[]} selectedOptions
27
+ * @property {{ id: string, title: string, handle: string, vendor: string, featuredImage: Image | null }} product
28
+ */
29
+ /**
30
+ * @typedef {object} LinePlanAllocation
31
+ * @property {{ id: string, name: string, description: string | null, recurringDeliveries: boolean,
32
+ * options: { name: string, value: string }[] }} sellingPlan
33
+ * @property {Money} checkoutChargeAmount
34
+ * @property {{ price: Money, compareAtPrice: Money, perDeliveryPrice: Money }[]} priceAdjustments
35
+ */
36
+ /**
37
+ * @typedef {object} ShopifyCartLine
38
+ * @property {string} id
39
+ * @property {number} quantity
40
+ * @property {Attribute[]} attributes
41
+ * @property {{ totalAmount: Money, subtotalAmount: Money, amountPerQuantity: Money,
42
+ * compareAtAmountPerQuantity: Money | null }} cost
43
+ * @property {{ discountedAmount: Money }[]} discountAllocations
44
+ * @property {LinePlanAllocation | null} sellingPlanAllocation
45
+ * @property {CartVariant} merchandise
46
+ * @property {{ id: string, quantity: number, merchandise: { id: string, title: string,
47
+ * product: { title: string, handle: string } } }[]} [lineComponents] Only on a bundle (`ComponentizableCartLine`).
48
+ */
49
+ /**
50
+ * @typedef {object} ShopifyCart The fields of `fragment TetherCart`.
51
+ * @property {string} id
52
+ * @property {string} checkoutUrl
53
+ * @property {number} totalQuantity
54
+ * @property {string | null} note
55
+ * @property {string} createdAt
56
+ * @property {string} updatedAt
57
+ * @property {Attribute[]} attributes
58
+ * @property {{ countryCode: string | null, email: string | null }} buyerIdentity
59
+ * @property {{ code: string, applicable: boolean }[]} discountCodes
60
+ * @property {{ subtotalAmount: Money, totalAmount: Money, checkoutChargeAmount: Money }} cost
61
+ * @property {{ nodes: ShopifyCartLine[] }} lines
62
+ */
63
+
64
+ /**
65
+ * @typedef {{ kind: 'unknown' }
66
+ * | { kind: 'empty' }
67
+ * | { kind: 'ready', cart: ShopifyCart }
68
+ * | { kind: 'error', error: unknown, cart: ShopifyCart | null }} CartStatus
69
+ * `unknown` until the saved cart has loaded (the page's own markup shows); `empty` without a cart (none saved, or
70
+ * Shopify no longer has it); `error` after a failed load or change, with the cart as Shopify last sent it.
71
+ */
72
+
73
+ /** @typedef {{ quantity: number }} PendingQuantity What one quick +/- click asked for; a newer click replaces it. */
74
+
75
+ /**
76
+ * @typedef {object} CustomerLink What the account part sets while a customer is logged in.
77
+ * @property {string | null} accessToken The Customer Account API token: carts created now belong to the customer.
78
+ * @property {(() => Promise<string | null>) | null} beforeCheckout The account's own checkout URL; `null` from it, or
79
+ * no function, uses the cart's.
80
+ */
81
+
82
+ /**
83
+ * @typedef {object} CartMeta The second argument of a `subscribe` listener.
84
+ * @property {boolean} open
85
+ * @property {number} count
86
+ * @property {boolean} loaded
87
+ * @property {readonly CartWarning[]} warnings
88
+ */
89
+
90
+ /** @type {CartStatus} */
91
+ let status = { kind: 'unknown' };
92
+
93
+ /** @type {readonly CartWarning[]} Shopify's notes on the last change. */
94
+ let warnings = [];
95
+
96
+ /** @type {Map<string, PendingQuantity>} Line id → what quick +/- clicks asked for, until Shopify has answered. */
97
+ export const pendingQuantities = new Map();
98
+
99
+ /** @type {CustomerLink} */
100
+ export const customer = { accessToken: null, beforeCheckout: null };
101
+
102
+ /** @returns {CartStatus} */
103
+ export const cartStatus = () => status;
104
+
105
+ /** @returns {ShopifyCart | null} The cart as Shopify last sent it. */
106
+ export function currentCart() {
107
+ switch (status.kind) {
108
+ case 'ready':
109
+ case 'error':
110
+ return status.cart;
111
+ case 'unknown':
112
+ case 'empty':
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /** @returns {readonly CartWarning[]} */
118
+ export const currentWarnings = () => warnings;
119
+
120
+ /**
121
+ * @param {ShopifyCart | null} cart
122
+ * @returns {CartStatus}
123
+ */
124
+ const statusOf = (cart) => (cart ? { kind: 'ready', cart } : { kind: 'empty' });
125
+
126
+ const seenKey = () => `${cfg.storageKey}:seen`;
127
+
128
+ /**
129
+ * localStorage throws in private mode and where the visitor blocks site data: the cart then lasts for this page.
130
+ *
131
+ * @param {string} key
132
+ * @returns {string | null}
133
+ */
134
+ function readStorageIfAllowed(key) {
135
+ try {
136
+ return localStorage.getItem(key);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Like `readStorageIfAllowed`: without storage, nothing is remembered.
144
+ *
145
+ * @param {string} key
146
+ * @param {string | null} value `null` removes the key.
147
+ */
148
+ function writeStorageIfAllowed(key, value) {
149
+ try {
150
+ if (value === null) localStorage.removeItem(key);
151
+ else localStorage.setItem(key, value);
152
+ } catch {
153
+ // Private mode or blocked storage: see readStorageIfAllowed.
154
+ }
155
+ }
156
+
157
+ /** @returns {string | null} The id of the visitor's cart, kept across pages. */
158
+ export const savedCartId = () => readStorageIfAllowed(cfg.storageKey) || null;
159
+
160
+ /** Forgets the saved cart, which Shopify no longer has. */
161
+ export function forgetSavedCart() {
162
+ writeStorageIfAllowed(cfg.storageKey, null);
163
+ }
164
+
165
+ /**
166
+ * Keeps a cart Shopify sent without showing it, for an answer a newer change will replace.
167
+ *
168
+ * @param {ShopifyCart | null} cart
169
+ */
170
+ export function keepQuietly(cart) {
171
+ status = statusOf(cart);
172
+ if (cart) writeStorageIfAllowed(cfg.storageKey, cart.id);
173
+ }
174
+
175
+ /** Renders the document with the store's cart. */
176
+ export function render() {
177
+ renderCart({
178
+ cart: currentCart(),
179
+ warnings,
180
+ pending: pendingQuantities,
181
+ failed: status.kind === 'error',
182
+ });
183
+ }
184
+
185
+ /**
186
+ * @param {string} reason The `reason` of `tether:cart-updated`.
187
+ */
188
+ export function announce(reason) {
189
+ render();
190
+ emit('cart-updated', { cart: currentCart(), reason });
191
+ }
192
+
193
+ /**
194
+ * Keeps a cart Shopify sent after a change, shows it, and tells this page and the visitor's other tabs.
195
+ *
196
+ * @param {ShopifyCart | null} cart
197
+ * @param {string} reason
198
+ * @returns {ShopifyCart | null} `cart`.
199
+ */
200
+ export function commit(cart, reason) {
201
+ keepQuietly(cart);
202
+ // Other tabs reload the cart on this storage event; the next page shows the count before its cart has loaded.
203
+ writeStorageIfAllowed(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
204
+ announce(reason);
205
+ return cart;
206
+ }
207
+
208
+ /**
209
+ * The outcome of loading the saved cart: the cart, `null` when there is none, or the failure.
210
+ *
211
+ * @param {{ kind: 'loaded', cart: ShopifyCart | null } | { kind: 'failed', error: unknown }} outcome
212
+ */
213
+ export function settleLoad(outcome) {
214
+ status = outcome.kind === 'loaded' ? statusOf(outcome.cart) : { kind: 'error', error: outcome.error, cart: null };
215
+ announce('load');
216
+ }
217
+
218
+ /**
219
+ * Sets or clears the failure without rendering, for `Tether.cart.lastError`, which the public types declare writable.
220
+ *
221
+ * @param {unknown} error `null` or `undefined` clears it.
222
+ */
223
+ export function setFailure(error) {
224
+ const cart = currentCart();
225
+ status = error == null ? statusOf(cart) : { kind: 'error', error, cart };
226
+ }
227
+
228
+ /** Until the saved cart has loaded, the counts show the count it had on the last page instead of the page's 0. */
229
+ export function showLastKnownCount() {
230
+ if (status.kind !== 'unknown') return;
231
+ const count = readStorageIfAllowed(seenKey())?.split('|')[1];
232
+ if (count) showSavedCount(count);
233
+ }
234
+
235
+ /**
236
+ * Sets Shopify's notes on the last change: each message once, in the site's words (`warningText`).
237
+ *
238
+ * @param {readonly CartWarning[]} notes
239
+ */
240
+ export function setWarnings(notes) {
241
+ warnings = notes
242
+ .filter((note, index) => notes.findIndex((other) => other.message === note.message) === index)
243
+ .map((note) => ({ ...note, message: cfg.warningText?.(note) || note.message }));
244
+ if (warnings.length) emit('cart-warning', { warnings });
245
+ }
246
+
247
+ /** The queue only waits for a task: its result, or failure, reaches the caller through the promise `run` returns. */
248
+ const waitOnly = () => undefined;
249
+
250
+ /**
251
+ * @returns {<T>(task: () => Promise<T>) => Promise<T>} A runner that starts each task once the one before has
252
+ * settled, whether it succeeded or failed.
253
+ */
254
+ export function serial() {
255
+ /** @type {Promise<unknown>} */
256
+ let previous = Promise.resolve();
257
+ return (task) => {
258
+ const run = previous.then(task);
259
+ previous = run.then(waitOnly, waitOnly);
260
+ return run;
261
+ };
262
+ }
263
+
264
+ const runInOrder = serial();
265
+
266
+ /**
267
+ * Runs a cart change after the ones before it, so quick clicks never race. A failure sets the `error` status (shown by
268
+ * `tether-show="cart-error"` until the next change succeeds), emits `tether:cart-error` and rejects.
269
+ *
270
+ * @template T
271
+ * @param {() => Promise<T>} task
272
+ * @returns {Promise<T>}
273
+ */
274
+ export function enqueue(task) {
275
+ return runInOrder(task).then(
276
+ (value) => {
277
+ if (status.kind === 'error') {
278
+ status = statusOf(status.cart);
279
+ render();
280
+ }
281
+ return value;
282
+ },
283
+ (/** @type {unknown} */ error) => {
284
+ status = { kind: 'error', error, cart: currentCart() };
285
+ render();
286
+ emit('cart-error', { error });
287
+ throw error;
288
+ },
289
+ );
290
+ }
291
+
292
+ /**
293
+ * @param {ShopifyCartLine} line
294
+ * @returns {number} The quantity to show for `line`.
295
+ */
296
+ export const pendingQuantityOf = (line) => quantityOf(line, pendingQuantities);
297
+
298
+ /** @returns {CartMeta} */
299
+ const meta = () => ({
300
+ open: isOpen(),
301
+ count: currentCart()?.totalQuantity ?? 0,
302
+ loaded: status.kind !== 'unknown',
303
+ warnings,
304
+ });
305
+
306
+ /**
307
+ * Calls `listener` now and after every change of the cart or the drawer.
308
+ *
309
+ * @param {(cart: ShopifyCart | null, meta: CartMeta) => void} listener
310
+ * @returns {() => void} Stops listening.
311
+ */
312
+ export function subscribe(listener) {
313
+ const events = ['cart-updated', 'cart-toggled'].map((name) => `${cfg.prefix}:${name}`);
314
+ const call = () => {
315
+ listener(currentCart(), meta());
316
+ };
317
+ events.forEach((name) => {
318
+ document.addEventListener(name, call);
319
+ });
320
+ call();
321
+ return () => {
322
+ events.forEach((name) => {
323
+ document.removeEventListener(name, call);
324
+ });
325
+ };
326
+ }