@occluze/tether 0.6.7 → 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.
Files changed (250) hide show
  1. package/README.md +11 -11
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
  4. package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
  5. package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
  6. package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
  7. package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
  8. package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
  9. package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
  10. package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
  11. package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
  12. package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
  13. package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
  14. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  15. package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
  16. package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
  17. package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
  18. package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
  19. package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
  20. package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
  21. package/dist/esm/chunks/search-UJ73YBQ5.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 +55 -39
  25. package/dist/tether.account-pages.js +781 -495
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +462 -381
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +299 -194
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +354 -262
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +82 -45
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3653 -2116
  36. package/dist/tether.markets.js +238 -150
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +3 -3
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +207 -136
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  43. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  44. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  45. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  46. package/dist/types/server/src/shared/json.d.ts +25 -0
  47. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  48. package/dist/types/src/account/addresses-view.d.ts +5 -0
  49. package/dist/types/src/account/answers.d.ts +40 -0
  50. package/dist/types/src/account/markup.d.ts +19 -0
  51. package/dist/types/src/account/operations.d.ts +41 -0
  52. package/dist/types/src/account/order-lines.d.ts +13 -0
  53. package/dist/types/src/account/order-list.d.ts +12 -0
  54. package/dist/types/src/account/order-page.d.ts +6 -0
  55. package/dist/types/src/account/page-answers.d.ts +182 -0
  56. package/dist/types/src/account/page-events.d.ts +2 -0
  57. package/dist/types/src/account/server-client.d.ts +60 -0
  58. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  59. package/dist/types/src/account-api.d.ts +18 -0
  60. package/dist/types/src/account-marks.d.ts +15 -0
  61. package/dist/types/src/account-pages.d.ts +8 -0
  62. package/dist/types/src/account.d.ts +64 -0
  63. package/dist/types/src/amounts.d.ts +12 -0
  64. package/dist/types/src/analytics-shopify.d.ts +41 -0
  65. package/dist/types/src/analytics.d.ts +2 -0
  66. package/dist/types/src/api-version.d.ts +28 -0
  67. package/dist/types/src/astro.d.ts +42 -0
  68. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  69. package/dist/types/src/cart/checkout.d.ts +16 -0
  70. package/dist/types/src/cart/drawer.d.ts +18 -0
  71. package/dist/types/src/cart/mutations.d.ts +79 -0
  72. package/dist/types/src/cart/render.d.ts +17 -0
  73. package/dist/types/src/cart/store.d.ts +182 -0
  74. package/dist/types/src/cart.d.ts +106 -0
  75. package/dist/types/src/client.d.ts +100 -0
  76. package/dist/types/src/collection.d.ts +25 -0
  77. package/dist/types/src/config-sections.d.ts +40 -0
  78. package/dist/types/src/config.d.ts +185 -0
  79. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  80. package/dist/types/src/controls/discount.d.ts +2 -0
  81. package/dist/types/src/controls/extras.d.ts +5 -0
  82. package/dist/types/src/controls/navigation.d.ts +12 -0
  83. package/dist/types/src/diagnose-markup.d.ts +19 -0
  84. package/dist/types/src/diagnose.d.ts +60 -0
  85. package/dist/types/src/dom.d.ts +75 -0
  86. package/dist/types/src/extras.d.ts +21 -0
  87. package/dist/types/src/fragments.d.ts +158 -0
  88. package/dist/types/src/globals.d.ts +15 -0
  89. package/dist/types/src/images.d.ts +20 -0
  90. package/dist/types/src/index.d.ts +167 -0
  91. package/dist/types/src/initial.d.ts +56 -0
  92. package/dist/types/src/lazy.d.ts +28 -0
  93. package/dist/types/src/loader.d.ts +84 -0
  94. package/dist/types/src/market-codes.d.ts +6 -0
  95. package/dist/types/src/markets-ui.d.ts +32 -0
  96. package/dist/types/src/markets.d.ts +72 -0
  97. package/dist/types/src/markup.d.ts +131 -0
  98. package/dist/types/src/mounting.d.ts +46 -0
  99. package/dist/types/src/page-market.d.ts +27 -0
  100. package/dist/types/src/presets/barba.d.ts +56 -0
  101. package/dist/types/src/product-events.d.ts +2 -0
  102. package/dist/types/src/product-render.d.ts +17 -0
  103. package/dist/types/src/product-shape.d.ts +88 -0
  104. package/dist/types/src/product-state.d.ts +110 -0
  105. package/dist/types/src/product.d.ts +66 -0
  106. package/dist/types/src/repeaters.d.ts +16 -0
  107. package/dist/types/src/search.d.ts +33 -0
  108. package/dist/types/src/selling-plans.d.ts +64 -0
  109. package/dist/types/src/server.d.ts +88 -0
  110. package/dist/types/src/shopify-values.d.ts +46 -0
  111. package/dist/types/src/status.d.ts +33 -0
  112. package/dist/types/src/storage.d.ts +12 -0
  113. package/dist/types/src/store-html.d.ts +39 -0
  114. package/dist/types/src/storefront/catalog.d.ts +38 -0
  115. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  116. package/dist/types/src/variant-render.d.ts +14 -0
  117. package/package.json +42 -22
  118. package/src/account/addresses-view.ts +58 -0
  119. package/src/account/answers.ts +79 -0
  120. package/src/account/markup.ts +36 -0
  121. package/src/account/operations.ts +145 -0
  122. package/src/account/order-lines.ts +75 -0
  123. package/src/account/order-list.ts +107 -0
  124. package/src/account/order-page.ts +137 -0
  125. package/src/account/page-answers.ts +317 -0
  126. package/src/account/page-events.ts +253 -0
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/subscriptions-view.ts +115 -0
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/account-pages.ts +81 -0
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/analytics-shopify.ts +246 -0
  135. package/src/analytics.ts +315 -0
  136. package/src/api-version.ts +79 -0
  137. package/src/astro.ts +272 -0
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/drawer.ts +125 -0
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/render.ts +266 -0
  143. package/src/cart/store.ts +279 -0
  144. package/src/cart.ts +345 -0
  145. package/src/client.ts +391 -0
  146. package/src/collection.ts +175 -0
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/cart-controls.ts +123 -0
  150. package/src/controls/discount.ts +75 -0
  151. package/src/controls/extras.ts +54 -0
  152. package/src/controls/navigation.ts +77 -0
  153. package/src/diagnose-markup.ts +298 -0
  154. package/src/diagnose.ts +294 -0
  155. package/src/dom.ts +268 -0
  156. package/src/extras.ts +136 -0
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/images.ts +91 -0
  160. package/src/index.ts +415 -0
  161. package/src/initial.ts +101 -0
  162. package/src/lazy-iife.ts +197 -0
  163. package/src/lazy.ts +42 -0
  164. package/src/loader.ts +363 -0
  165. package/src/market-codes.ts +6 -0
  166. package/src/markets-ui.ts +227 -0
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/mounting.ts +264 -0
  170. package/src/page-market.ts +110 -0
  171. package/src/part-integrity.ts +7 -0
  172. package/src/parts/account-pages.ts +7 -0
  173. package/src/parts/account.ts +7 -0
  174. package/src/parts/analytics.ts +7 -0
  175. package/src/parts/diagnose.ts +7 -0
  176. package/src/parts/extras.ts +7 -0
  177. package/src/parts/markets.ts +7 -0
  178. package/src/parts/search.ts +7 -0
  179. package/src/presets/barba.ts +194 -0
  180. package/src/product-events.ts +115 -0
  181. package/src/product-render.ts +218 -0
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/product.ts +308 -0
  185. package/src/repeaters.ts +161 -0
  186. package/src/search.ts +305 -0
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/shopify-values.ts +91 -0
  190. package/src/status.ts +106 -0
  191. package/src/storage.ts +34 -0
  192. package/src/store-html.ts +359 -0
  193. package/src/storefront/catalog.ts +196 -0
  194. package/src/storefront/json-ld.ts +144 -0
  195. package/src/variant-render.ts +139 -0
  196. package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
  197. package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
  198. package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
  199. package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
  200. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  201. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  202. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  203. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  204. package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
  205. package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
  206. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  207. package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
  208. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  209. package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
  210. package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
  211. package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
  212. package/dist/esm/chunks/search-NILMGYXG.js +0 -1
  213. package/src/account-pages.js +0 -318
  214. package/src/account.js +0 -385
  215. package/src/analytics.js +0 -232
  216. package/src/api-version.js +0 -35
  217. package/src/astro.js +0 -101
  218. package/src/cart.js +0 -559
  219. package/src/client.js +0 -131
  220. package/src/collection.js +0 -114
  221. package/src/config.js +0 -85
  222. package/src/diagnose.js +0 -246
  223. package/src/dom.js +0 -288
  224. package/src/extras.js +0 -63
  225. package/src/fragments.js +0 -144
  226. package/src/index.js +0 -395
  227. package/src/initial.js +0 -75
  228. package/src/lazy-iife.js +0 -46
  229. package/src/lazy.js +0 -18
  230. package/src/loader.js +0 -198
  231. package/src/markets-ui.js +0 -90
  232. package/src/markets.js +0 -133
  233. package/src/mounting.js +0 -154
  234. package/src/parts/account-pages.js +0 -3
  235. package/src/parts/account.js +0 -3
  236. package/src/parts/analytics.js +0 -3
  237. package/src/parts/diagnose.js +0 -3
  238. package/src/parts/extras.js +0 -3
  239. package/src/parts/markets.js +0 -3
  240. package/src/parts/search.js +0 -3
  241. package/src/presets/barba.js +0 -100
  242. package/src/product.js +0 -570
  243. package/src/search.js +0 -153
  244. package/src/selling-plans.js +0 -94
  245. package/src/server.js +0 -252
  246. package/src/status.js +0 -66
  247. package/types/astro.d.ts +0 -17
  248. package/types/barba.d.ts +0 -16
  249. package/types/index.d.ts +0 -426
  250. package/types/server.d.ts +0 -110
package/src/index.js DELETED
@@ -1,395 +0,0 @@
1
- /**
2
- * Tether — Shopify <-> Webflow (or any HTML) sync via the Storefront API.
3
- *
4
- * <script>window.TETHER_CONFIG = { domain: 'x.myshopify.com', token: '...' }</script>
5
- * <script src="tether.min.js" defer></script>
6
- *
7
- * With page transitions (Barba, Swup): set autoMount: false, call
8
- * Tether.init() once and Tether.mount(container) after every page enter.
9
- */
10
- import { cfg, configure, isConfigured, log, warn, reportError } from './config.js';
11
- import { gql, TetherError, withContext } from './client.js';
12
- import { Cart, focusables } from './cart.js';
13
- import { scopes } from './product.js';
14
- import { ListScope, listSelector } from './collection.js';
15
- import {
16
- getProduct, getProducts, getRecommendations, clearProductCache, primeProduct, normalizeProductKey,
17
- } from './loader.js';
18
- import * as plans from './selling-plans.js';
19
- import { resolveProductKey } from './status.js';
20
- import { attr, sel, get, qsa, emit, formatMoney, busy, parseJSONAttr, toAttributes, imageUrl } from './dom.js';
21
- import { loadPart, coreModules } from './lazy.js';
22
- import { mountedLists, liveLists, pruneLive, within, mountProduct, retryFailed, hydrateFromPage, settleMarket, trackMarket } from './mounting.js';
23
-
24
- export const VERSION = '0.6.7';
25
-
26
- let initPromise = null;
27
- let observer = null;
28
- let mountedBody = null; // the page body mount(document) last ran on (Astro's first page-load repeats it)
29
-
30
- /** Mount every product scope and product list inside `root`. Safe to call repeatedly. */
31
- export function mount(root = document) {
32
- if (!isConfigured()) return 0;
33
- if (root === document) mountedBody = document.body;
34
- let count = 0;
35
- pruneLive(); // page transitions detach old roots: don't keep them alive
36
- hydrateFromPage(root); // products the server already rendered: no request needed
37
-
38
- within(root, listSelector()).forEach((el) => {
39
- if (mountedLists.has(el)) return;
40
- mountedLists.add(el);
41
- count++;
42
- const list = new ListScope(el);
43
- liveLists.add(list);
44
- el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
45
- trackMarket(list.load());
46
- });
47
-
48
- within(root, sel('product')).forEach((el) => { if (mountProduct(el)) count++; });
49
-
50
- // The country select lives in the optional markets part: load it only when the page uses it.
51
- if (within(root, `${sel('country-select')},${sel('country-bind')}`).length) {
52
- loadPart('markets').then((m) => m.renderCountrySelects(root)).catch((err) => reportError(err, 'markets'));
53
- }
54
-
55
- // Customer accounts: their own optional part, when configured and the page (or a login) needs it.
56
- if ((cfg.account?.clientId || cfg.account?.server) && (within(root, ['account-login', 'account-bind', 'account-link', 'orders', 'order', 'addresses', 'subscriptions', 'profile-form', 'address-form'].map((n) => `[${attr(n)}]`).join(',') + `,[${attr('show')}^="logged-"]`).length
57
- || /[?&]code=/.test(window.location.search) || hasAccountSession())) {
58
- loadPart('account').then((m) => m.mountAccount(root)).catch((err) => reportError(err, 'account'));
59
- }
60
-
61
- // Search as you type: its own optional part, only for pages that have it.
62
- if (within(root, sel('search')).length) {
63
- loadPart('search').then((m) => m.mountSearch(root)).catch((err) => reportError(err, 'search'));
64
- }
65
-
66
-
67
- settleMarket();
68
- Cart.render();
69
- log(`mounted ${count} scope(s)`, root);
70
- return count;
71
- }
72
-
73
- // ---------------------------------------------------------------------------
74
- // Markets: country select + switching
75
- // ---------------------------------------------------------------------------
76
-
77
- /** Call before a page transition removes `root` (closes the drawer etc.). */
78
- export function unmount() {
79
- Cart.close();
80
- }
81
-
82
- export function getScope(el) {
83
- const root = el?.closest?.(sel('product'));
84
- return root ? scopes.get(root) || null : null;
85
- }
86
-
87
- // ---------------------------------------------------------------------------
88
- // Document-wide controls (delegated, so they survive page swaps)
89
- // ---------------------------------------------------------------------------
90
-
91
- let controlsBound = false;
92
-
93
- function bindControls() {
94
- if (controlsBound) return;
95
- controlsBound = true;
96
-
97
- document.addEventListener('click', (e) => {
98
- const t = e.target;
99
- if (!t?.closest) return;
100
-
101
- if (t.closest(sel('cart-open'))) { e.preventDefault(); Cart.toggle(); return; }
102
- if (t.closest(sel('cart-close'))) { e.preventDefault(); Cart.close(); return; }
103
-
104
- const clear = t.closest(sel('cart-clear'));
105
- if (clear) { e.preventDefault(); busy(clear, () => Cart.clear().catch((err) => reportError(err, 'cart clear'))); return; }
106
-
107
- const checkout = t.closest(sel('checkout'));
108
- if (checkout) { e.preventDefault(); busy(checkout, () => Cart.checkout()); return; }
109
-
110
- const line = t.closest(dataKeySel());
111
- if (line && line.closest(sel('cart-lines'))) {
112
- const id = line.getAttribute(`data-${cfg.prefix}-key`);
113
- const current = Cart.lines.find((l) => l.id === id);
114
- const run = (fn) => { e.preventDefault(); fn().catch((err) => reportError(err, 'cart line')); };
115
- if (t.closest(sel('line-remove'))) { line.classList.add('is-busy'); return run(() => Cart.removeLine(id)); }
116
- if (current && t.closest(sel('line-plus'))) return run(() => Cart.changeQuantity(id, 1));
117
- if (current && t.closest(sel('line-minus'))) return run(() => Cart.changeQuantity(id, -1));
118
- }
119
-
120
- const removeCode = t.closest(sel('discount-remove'));
121
- if (removeCode) {
122
- e.preventDefault();
123
- const code = removeCode.closest(sel('discount-code'))?.getAttribute(attr('discount-code'));
124
- if (code) Cart.removeDiscount(code).catch((err) => reportError(err, 'discount'));
125
- return;
126
- }
127
-
128
- const groupBtn = t.closest(sel('group-add'));
129
- if (groupBtn) { e.preventDefault(); busy(groupBtn, () => addGroup(groupBtn)); }
130
- });
131
-
132
- document.addEventListener('change', (e) => {
133
- const t = e.target;
134
- if (!t?.closest) return;
135
- const lineEl = t.closest(dataKeySel());
136
- const id = lineEl?.getAttribute(`data-${cfg.prefix}-key`);
137
- // The line shows is-busy / aria-busy until Shopify has answered (removed by the next render).
138
- const busyLine = (p, what) => { lineEl.classList.add('is-busy'); lineEl.setAttribute('aria-busy', 'true'); p.catch((err) => reportError(err, what)); };
139
- if (id && t.matches(`input${sel('line', 'quantity')}`)) {
140
- busyLine(Cart.updateLine(id, parseInt(t.value, 10) || 0), 'cart line');
141
- } else if (id && t.matches(`select${sel('line-plan')}`)) {
142
- busyLine(Cart.updateLine(id, { sellingPlanId: t.value || null }), 'cart line plan');
143
- } else if (t.matches(`select${sel('country-select')}`)) {
144
- if (t.value) setCountry(t.value).catch((err) => reportError(err, 'country'));
145
- } else if (t.matches(sel('cart-note'))) {
146
- Cart.setNote(t.value).catch((err) => reportError(err, 'cart note'));
147
- }
148
- });
149
-
150
- document.addEventListener('submit', (e) => {
151
- const form = e.target.closest?.(sel('discount-form'));
152
- if (!form) return;
153
- e.preventDefault();
154
- const input = form.querySelector(sel('discount-input')) || form.querySelector('input');
155
- const code = input?.value.trim();
156
- if (!code) return;
157
- busy(form, async () => {
158
- const mark = (bad) => {
159
- form.classList.toggle('is-invalid', bad);
160
- if (input) { if (bad) input.setAttribute('aria-invalid', 'true'); else input.removeAttribute('aria-invalid'); }
161
- };
162
- try {
163
- await Cart.applyDiscount(code);
164
- mark(false);
165
- emit('discount-applied', { code }, form);
166
- if (input) input.value = '';
167
- } catch (err) {
168
- mark(true);
169
- if (err?.code === 'INVALID_DISCOUNT') emit('discount-invalid', { code }, form); else reportError(err, 'discount');
170
- }
171
- });
172
- }, true); // capture: before a router (Astro's ClientRouter) turns the submit into a navigation
173
- document.addEventListener('input', (e) => {
174
- const f = e.target.closest?.(sel('discount-form'));
175
- if (f) { f.classList.remove('is-invalid'); e.target.removeAttribute?.('aria-invalid'); }
176
- });
177
-
178
- // Other tabs: the cart or the market changed there (handled by the extras part, loaded then).
179
- window.addEventListener('storage', (e) => {
180
- if (cfg.domain && e.key?.startsWith(`${cfg.prefix}:`)) loadPart('extras').then((m) => m.onStorage(e)).catch(() => {});
181
- });
182
- // Shareable variant links: only when turned on.
183
- document.addEventListener(`${cfg.prefix}:variant-changed`, (e) => {
184
- const root = e.target;
185
- if (cfg.variantUrl || root.hasAttribute?.(attr('variant-url'))) loadPart('extras').then((m) => m.writeVariantUrl(root, e.detail)).catch(() => {});
186
- });
187
-
188
- // Astro View Transitions (<ClientRouter />): mount every new page, close the cart while leaving.
189
- if (cfg.astro !== false) {
190
- // Astro fires this on the first load too: wait for init (the visitor's saved market) first.
191
- document.addEventListener('astro:page-load', () => (initPromise || Promise.resolve()).then(() => {
192
- // Navigated to a page of another market (e.g. /de/): switch first, then mount in that market.
193
- // Back on a normal page after one: the visitor's own choice, else the default market.
194
- let target = pageMarket();
195
- if (!target && onPageMarket) {
196
- try { target = JSON.parse(localStorage.getItem(`${cfg.prefix}:market:${cfg.domain}`) || 'null'); } catch { target = null; }
197
- target = target?.country ? target : baseMarket;
198
- }
199
- onPageMarket = Boolean(pageMarket());
200
- const differs = target?.country && (String(target.country).toUpperCase() !== String(cfg.country || '').toUpperCase()
201
- || String(target.language || '').toUpperCase() !== String(cfg.language || '').toUpperCase());
202
- if (differs) setCountry(target.country, { language: target.language ?? null, locale: target.locale, remember: false }).then(() => mount(document), () => mount(document));
203
- else if (mountedBody !== document.body) mount(document); // the first load is already mounted: no second round of requests
204
- }));
205
- document.addEventListener('astro:before-swap', () => { mountedBody = null; unmount(); });
206
- }
207
-
208
- // Back online after a failed load: try the failed products and lists again.
209
- window.addEventListener('online', () => {
210
- retryFailed();
211
- liveLists.forEach((l) => { if (l.root.getAttribute(attr('state')) === 'error') l.load(); });
212
- });
213
-
214
- document.addEventListener('keydown', (e) => {
215
- if (!Cart.isOpen()) return;
216
- if (e.key === 'Escape') { Cart.close(); return; }
217
- if (e.key !== 'Tab' || cfg.cartFocus === false) return;
218
- // Keep Tab inside the open cart.
219
- const d = Cart.drawers().find((x) => x.classList.contains(cfg.cartDrawerOpenClass));
220
- const f = d ? focusables(d) : [];
221
- if (!f.length) return;
222
- const first = f[0];
223
- const last = f[f.length - 1];
224
- if (!d.contains(document.activeElement) || (e.shiftKey && document.activeElement === first) || (!e.shiftKey && document.activeElement === last)) {
225
- e.preventDefault();
226
- (e.shiftKey ? last : first).focus();
227
- }
228
- });
229
- }
230
-
231
- function dataKeySel() { return `[data-${cfg.prefix}-key]`; }
232
-
233
- /**
234
- * [tether-group] + [tether-group-add]: adds every product scope inside the
235
- * group in one request. Bundles, build-a-box, "complete the set".
236
- * Lines get attribute _group (and the group's tether-line-attributes).
237
- */
238
- export const addGroup = (btn) => loadPart('extras').then((m) => m.addGroup(btn));
239
-
240
- // ---------------------------------------------------------------------------
241
- // Init
242
- // ---------------------------------------------------------------------------
243
-
244
- let analyticsQueue = null;
245
-
246
- export function init(overrides = {}) {
247
- if (initPromise && !Object.keys(overrides).length) return initPromise;
248
- configure({ ...(window.TETHER_CONFIG || {}), ...overrides });
249
-
250
- if (!isConfigured()) {
251
- warn('TETHER_CONFIG is missing domain or token. Tether is idle.');
252
- initPromise = Promise.resolve(null);
253
- return initPromise;
254
- }
255
-
256
- bindControls();
257
- if (cfg.observe) startObserver();
258
- // Analytics (optional part): buffer Tether's events until it has loaded, so none are missed.
259
- if (cfg.analytics && !analyticsQueue) {
260
- analyticsQueue = [];
261
- ['product-ready', 'list-ready', 'cart-updated', 'cart-toggled', 'checkout', 'search'].forEach((n) => document.addEventListener(`${cfg.prefix}:${n}`, (e) => analyticsQueue.push(e)));
262
- document.addEventListener('astro:page-load', () => analyticsQueue.push(new CustomEvent(`${cfg.prefix}:page`)));
263
- loadPart('analytics').then((m) => m.startAnalytics(analyticsQueue)).catch((err) => reportError(err, 'analytics'));
264
- }
265
-
266
- initPromise = applyMarket(overrides).then(() => Cart.init()).then(async (cart) => {
267
- // A saved cart from another market: move it to this one, so prices match the page.
268
- const want = cfg.country ? String(cfg.country).toUpperCase() : null;
269
- if (cart && want && cart.buyerIdentity?.countryCode && cart.buyerIdentity.countryCode !== want) {
270
- try { cart = await Cart.setBuyerIdentity({ countryCode: want }, { create: false }); } catch (err) { reportError(err, 'cart country'); }
271
- }
272
- emit('ready', { cart });
273
- return api;
274
- });
275
- return initPromise;
276
- }
277
-
278
- /**
279
- * Detected market (saved choice, server hint, Webflow locale) wins over the static config.
280
- * Without markets.detect only the visitor's saved choice applies, read here without loading
281
- * the markets part.
282
- */
283
- /**
284
- * A page built for one market (e.g. /de/ pages): <meta name="tether-page-country" content="DE">,
285
- * tether-page-language, tether-page-locale. Wins over the visitor's saved choice, isn't remembered.
286
- */
287
- function pageMarket(doc = document) {
288
- const get = (n) => doc.querySelector(`meta[name="${cfg.prefix}-page-${n}"]`)?.getAttribute('content') || undefined;
289
- const country = get('country');
290
- return country ? { country: country.toUpperCase(), language: get('language'), locale: get('locale') } : null;
291
- }
292
-
293
- let baseMarket = null; // the configured market, before a visitor's or page's market applied
294
- let onPageMarket = false;
295
-
296
- async function applyMarket(overrides) {
297
- const m = cfg.markets || {};
298
- baseMarket = { country: cfg.country, language: cfg.language, locale: cfg.locale };
299
- const page = pageMarket();
300
- onPageMarket = Boolean(page);
301
- if (page) {
302
- cfg.country = page.country;
303
- cfg.language = page.language ? page.language.toUpperCase() : undefined;
304
- if (page.locale) cfg.locale = page.locale;
305
- return;
306
- }
307
- if (m.detect === false) return;
308
- let market = null;
309
- if (m.detect) {
310
- try { market = (await loadPart('markets')).detectMarket(m); } catch (err) { reportError(err, 'markets'); return; }
311
- } else if (m.remember !== false) {
312
- try { market = JSON.parse(localStorage.getItem(`${cfg.prefix}:market:${cfg.domain}`) || 'null'); } catch { /* none */ }
313
- if (market && !/^[A-Z]{2}$/.test(market.country || '')) market = null;
314
- }
315
- if (!market) return;
316
- if (market.country) cfg.country = market.country;
317
- if (market.language) cfg.language = market.language;
318
- if (market.locale && !overrides.locale) cfg.locale = market.locale; // the visitor's market wins over the default
319
- log('market', market);
320
- }
321
-
322
- function hasAccountSession() {
323
- try { return Boolean(sessionStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account-hint:${cfg.domain}`]); } catch { return false; }
324
- }
325
-
326
- /** Customer accounts (optional part, loaded on first use). See src/account.js. */
327
- const accountApi = new Proxy({}, { get: (_, n) => (...a) => loadPart('account').then((m) => m[n](...a)) });
328
-
329
- /** Tether.setCountry('DE'): loads the markets part on first use. */
330
- export function setCountry(code, opts) {
331
- return loadPart('markets').then((m) => m.setCountry(code, opts));
332
- }
333
-
334
- function startObserver() {
335
- if (observer || typeof MutationObserver === 'undefined') return;
336
- let t = null;
337
- const queued = new Set();
338
- observer = new MutationObserver((muts) => {
339
- muts.forEach((m) => m.addedNodes.forEach((n) => { if (n.nodeType === 1) queued.add(n); }));
340
- clearTimeout(t);
341
- t = setTimeout(() => { queued.forEach((n) => { if (n.isConnected) mount(n); }); queued.clear(); }, 50);
342
- });
343
- observer.observe(document.body, { childList: true, subtree: true });
344
- }
345
-
346
- export const api = {
347
- VERSION,
348
- init,
349
- mount,
350
- unmount,
351
- get config() { return cfg; },
352
- get ready() { return initPromise || Promise.resolve(null); },
353
- query: gql,
354
- withContext,
355
- cart: Cart,
356
- getProduct,
357
- getProducts,
358
- getRecommendations,
359
- getScope,
360
- addGroup,
361
- account: accountApi,
362
- primeProduct,
363
- normalizeProductKey,
364
- clearProductCache,
365
- formatMoney,
366
- imageUrl,
367
- plans,
368
- TetherError,
369
- resolveProductKey,
370
- setCountry,
371
- detectMarket: (markets) => loadPart('markets').then((m) => m.detectMarket(markets || cfg.markets)),
372
- getLocalization: () => loadPart('markets').then((m) => m.getLocalization()),
373
- diagnose: (opts) => loadPart('diagnose').then((m) => m.diagnose({ ...opts, version: VERSION })),
374
- __core: coreModules,
375
- __parts: {},
376
- on(name, fn) {
377
- const h = (e) => fn(e.detail, e);
378
- document.addEventListener(`${cfg.prefix}:${name}`, h);
379
- return () => document.removeEventListener(`${cfg.prefix}:${name}`, h);
380
- },
381
- };
382
-
383
- export default api;
384
-
385
- // Auto-start for the plain <script> setup.
386
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
387
- window.Tether = api;
388
- // Not with a framework integration (Astro), which starts Tether itself.
389
- const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false && !window.TETHER_INTEGRATION;
390
- if (auto) {
391
- const start = () => init().then(() => mount(document));
392
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
393
- else start();
394
- }
395
- }
package/src/initial.js DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * What the browser script will show first for a product, so a server can render the same
3
- * (no flash when Tether takes over): the selected variant, price, purchase mode and conditions.
4
- * Same rules as src/product.js: first available variant (or the one in ?variant=), subscription
5
- * when the product requires a plan or defaultPurchase is 'subscription', low stock at 5 or fewer.
6
- */
7
- import { GID, allPlans, allocationFor, findPlan, planPrice, planSavings } from './selling-plans.js';
8
-
9
- export function initialState(product, { variant, sellingPlan, defaultPurchase = 'one-time', lowStock = 5 } = {}) {
10
- const vs = product?.variants?.nodes || [];
11
- const gid = variant ? GID.variant(variant) : null;
12
- const v = (gid && vs.find((x) => x.id === gid)) || vs.find((x) => x.availableForSale) || vs[0] || null;
13
- if (!v) return null;
14
- const selected = Object.fromEntries((v.selectedOptions || []).map((o) => [o.name, o.value]));
15
-
16
- const plans = allPlans(product);
17
- const fromUrl = findPlan(product, sellingPlan);
18
- let plan = plans.length ? (fromUrl || plans[0]) : null;
19
- if (plan && !allocationFor(v, plan.id)) plan = plans.find((p) => allocationFor(v, p.id)) || plan;
20
- const wantsSub = product.requiresSellingPlan || fromUrl || defaultPurchase === 'subscription';
21
- const subscriptionAvailable = plans.some((p) => allocationFor(v, p.id));
22
- const purchase = plans.length && wantsSub ? 'subscription' : 'one-time';
23
- const sub = purchase === 'subscription' && subscriptionAvailable;
24
-
25
- const price = sub ? planPrice(v, plan.id) : v.price;
26
- const compareAt = sub ? (allocationFor(v, plan.id)?.priceAdjustments?.[0]?.compareAtPrice || v.compareAtPrice) : v.compareAtPrice;
27
- const onSale = Boolean(compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount));
28
- const q = v.quantityAvailable;
29
- const low = q != null && q > 0 && q <= lowStock;
30
- const savingsNow = sub ? planSavings(v, plan.id).percentage : 0;
31
-
32
- return {
33
- variant: v,
34
- selected,
35
- purchase,
36
- plan: sub ? plan : null,
37
- /** The plan list (tether-plan-list="all"): selected one gets is-selected, unavailable ones is-unavailable. */
38
- plans: plans.map((p) => ({
39
- id: p.id, name: p.name, price: planPrice(v, p.id), savings: planSavings(v, p.id).percentage,
40
- selected: sub && p.id === plan.id, available: Boolean(allocationFor(v, p.id)),
41
- })),
42
- /** Savings of the selected plan in % (tether-bind="subscription-savings"), 0 without one. */
43
- savings: plan ? planSavings(v, plan.id).percentage : 0,
44
- price,
45
- compareAtPrice: onSale ? compareAt : null,
46
- available: v.availableForSale,
47
- /** The tether-show conditions, e.g. hidden={!state.show['out-of-stock']}. */
48
- show: {
49
- 'in-stock': v.availableForSale,
50
- 'out-of-stock': !v.availableForSale,
51
- backorder: Boolean(v.availableForSale && v.currentlyNotInStock),
52
- 'on-sale': onSale,
53
- 'not-on-sale': !onSale,
54
- 'has-variants': vs.length > 1,
55
- 'single-variant': vs.length === 1,
56
- 'low-stock': low,
57
- 'plenty-in-stock': v.availableForSale && !low,
58
- 'subscription-available': subscriptionAvailable,
59
- 'subscription-unavailable': !subscriptionAvailable,
60
- 'subscription-only': Boolean(product.requiresSellingPlan),
61
- 'one-time-available': !product.requiresSellingPlan,
62
- 'subscription-selected': sub,
63
- 'one-time-selected': !sub,
64
- 'has-savings': sub ? savingsNow > 0 : onSale,
65
- },
66
- /** Option values without an available variant, as plain data (for frameworks that serialise props). */
67
- unavailable: Object.fromEntries((product.options || []).map((o) => [o.name, o.optionValues.map((x) => x.name).filter((n) => isUnavail(o.name, n))])),
68
- /** Option values with no available variant (given the other selected options): class is-unavailable. */
69
- isUnavailable: (name, value) => isUnavail(name, value),
70
- };
71
- function isUnavail(name, value) {
72
- return !vs.some((x) => x.availableForSale && x.selectedOptions.every((o) =>
73
- (o.name === name ? o.value === value : selected[o.name] === o.value)));
74
- }
75
- }
package/src/lazy-iife.js DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * <script> build: optional parts are separate files next to tether(.min).js, e.g.
3
- * tether.markets.min.js, loaded on first use. Override the folder with config.partsUrl.
4
- */
5
- import * as config from './config.js';
6
- import * as client from './client.js';
7
- import * as dom from './dom.js';
8
- import * as fragments from './fragments.js';
9
- import * as loader from './loader.js';
10
- import * as sellingPlans from './selling-plans.js';
11
- import * as status from './status.js';
12
- import * as cart from './cart.js';
13
- import * as product from './product.js';
14
- import * as collection from './collection.js';
15
- import * as mounting from './mounting.js';
16
-
17
- const { cfg } = config;
18
-
19
- /** The modules a part may use, shared instead of bundled twice (scripts/build.mjs maps imports to these). */
20
- export const coreModules = {
21
- 'config.js': config, 'client.js': client, 'dom.js': dom, 'fragments.js': fragments, 'loader.js': loader,
22
- 'selling-plans.js': sellingPlans, 'status.js': status, 'cart.js': cart, 'product.js': product,
23
- 'collection.js': collection, 'mounting.js': mounting,
24
- };
25
-
26
- const self = typeof document !== 'undefined' ? document.currentScript?.src || '' : '';
27
- const parts = {};
28
-
29
- export function loadPart(name) {
30
- if (parts[name]) return parts[name];
31
- const base = cfg.partsUrl || self.replace(/[^/]*$/, '');
32
- const min = /\.min\.js(\?|$)/.test(self) || !self ? '.min' : '';
33
- const url = `${base.replace(/\/?$/, '/')}tether.${name}${min}.js`;
34
- parts[name] = new Promise((resolve, reject) => {
35
- const s = document.createElement('script');
36
- s.src = url;
37
- s.async = true;
38
- s.onload = () => (window.Tether?.__parts?.[name] ? resolve(window.Tether.__parts[name]) : reject(new Error(`${url} did not register`)));
39
- s.onerror = () => { delete parts[name]; reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`)); };
40
- document.head.appendChild(s);
41
- });
42
- return parts[name];
43
- }
44
-
45
- // Parts that load other parts (account -> account-pages) use this same loader.
46
- coreModules['lazy.js'] = { loadPart, coreModules };
package/src/lazy.js DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * Optional parts that most pages never need, loaded on first use.
3
- * ESM / Node: dynamic import (bundlers split these into their own chunks).
4
- * The <script> build swaps this file for lazy-iife.js (see scripts/build.mjs).
5
- */
6
- const PARTS = {
7
- diagnose: () => import('./diagnose.js'),
8
- markets: () => import('./markets-ui.js'),
9
- search: () => import('./search.js'),
10
- extras: () => import('./extras.js'),
11
- account: () => import('./account.js'),
12
- 'account-pages': () => import('./account-pages.js'),
13
- analytics: () => import('./analytics.js'),
14
- };
15
- export const loadPart = (name) => PARTS[name]();
16
-
17
- // Only the <script> build shares its modules with separately loaded parts.
18
- export const coreModules = undefined;