@occluze/tether 0.6.8 → 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 (215) hide show
  1. package/README.md +28 -28
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
  4. package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
  5. package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
  6. package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
  7. package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
  8. package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
  9. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  10. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  11. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  12. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  13. package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
  14. package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
  15. package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
  16. package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
  17. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  18. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  19. package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
  20. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  21. package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
  22. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/presets/barba.esm.js +19 -23
  26. package/dist/tether.account-pages.js +332 -367
  27. package/dist/tether.account-pages.min.js +1 -1
  28. package/dist/tether.account.js +253 -492
  29. package/dist/tether.account.min.js +1 -1
  30. package/dist/tether.analytics.js +82 -89
  31. package/dist/tether.analytics.min.js +1 -1
  32. package/dist/tether.diagnose.js +52 -34
  33. package/dist/tether.diagnose.min.js +2 -2
  34. package/dist/tether.extras.js +42 -37
  35. package/dist/tether.extras.min.js +1 -1
  36. package/dist/tether.js +1361 -935
  37. package/dist/tether.markets.js +89 -91
  38. package/dist/tether.markets.min.js +1 -1
  39. package/dist/tether.min.js +0 -0
  40. package/dist/tether.min.js.map +4 -4
  41. package/dist/tether.search.js +60 -69
  42. package/dist/tether.search.min.js +1 -1
  43. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  44. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  45. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  46. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  47. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  48. package/dist/types/server/src/shared/json.d.ts +25 -0
  49. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  50. package/dist/types/src/account/addresses-view.d.ts +5 -0
  51. package/dist/types/src/account/answers.d.ts +40 -0
  52. package/dist/types/src/account/markup.d.ts +19 -0
  53. package/dist/types/src/account/operations.d.ts +41 -0
  54. package/dist/types/src/account/order-lines.d.ts +13 -0
  55. package/dist/types/src/account/order-list.d.ts +12 -0
  56. package/dist/types/src/account/order-page.d.ts +6 -0
  57. package/dist/types/src/account/page-answers.d.ts +182 -0
  58. package/dist/types/src/account/page-events.d.ts +2 -0
  59. package/dist/types/src/account/server-client.d.ts +60 -0
  60. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  61. package/dist/types/src/account-api.d.ts +18 -0
  62. package/dist/types/src/account-marks.d.ts +15 -0
  63. package/dist/types/src/account-pages.d.ts +8 -0
  64. package/dist/types/src/account.d.ts +64 -0
  65. package/dist/types/src/amounts.d.ts +12 -0
  66. package/dist/types/src/analytics-shopify.d.ts +41 -0
  67. package/dist/types/src/analytics.d.ts +2 -0
  68. package/dist/types/src/api-version.d.ts +28 -0
  69. package/dist/types/src/astro.d.ts +42 -0
  70. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  71. package/dist/types/src/cart/checkout.d.ts +16 -0
  72. package/dist/types/src/cart/drawer.d.ts +18 -0
  73. package/dist/types/src/cart/mutations.d.ts +85 -0
  74. package/dist/types/src/cart/render.d.ts +17 -0
  75. package/dist/types/src/cart/store.d.ts +185 -0
  76. package/dist/types/src/cart.d.ts +106 -0
  77. package/dist/types/src/client.d.ts +134 -0
  78. package/dist/types/src/collection.d.ts +25 -0
  79. package/dist/types/src/config-sections.d.ts +40 -0
  80. package/dist/types/src/config.d.ts +190 -0
  81. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  82. package/dist/types/src/controls/discount.d.ts +2 -0
  83. package/dist/types/src/controls/extras.d.ts +5 -0
  84. package/dist/types/src/controls/navigation.d.ts +12 -0
  85. package/dist/types/src/diagnose-markup.d.ts +19 -0
  86. package/dist/types/src/diagnose.d.ts +60 -0
  87. package/dist/types/src/dom.d.ts +75 -0
  88. package/dist/types/src/extras.d.ts +21 -0
  89. package/dist/types/src/fragments.d.ts +158 -0
  90. package/dist/types/src/globals.d.ts +15 -0
  91. package/dist/types/src/images.d.ts +20 -0
  92. package/dist/types/src/index.d.ts +167 -0
  93. package/dist/types/src/initial.d.ts +56 -0
  94. package/dist/types/src/lazy.d.ts +28 -0
  95. package/dist/types/src/loader.d.ts +84 -0
  96. package/dist/types/src/market-codes.d.ts +6 -0
  97. package/dist/types/src/markets-ui.d.ts +32 -0
  98. package/dist/types/src/markets.d.ts +72 -0
  99. package/dist/types/src/markup.d.ts +131 -0
  100. package/dist/types/src/mounting.d.ts +46 -0
  101. package/dist/types/src/page-market.d.ts +27 -0
  102. package/dist/types/src/presets/barba.d.ts +56 -0
  103. package/dist/types/src/product-events.d.ts +2 -0
  104. package/dist/types/src/product-render.d.ts +17 -0
  105. package/dist/types/src/product-shape.d.ts +90 -0
  106. package/dist/types/src/product-state.d.ts +110 -0
  107. package/dist/types/src/product.d.ts +66 -0
  108. package/dist/types/src/repeaters.d.ts +16 -0
  109. package/dist/types/src/search.d.ts +33 -0
  110. package/dist/types/src/selling-plans.d.ts +64 -0
  111. package/dist/types/src/server.d.ts +88 -0
  112. package/dist/types/src/shopify-values.d.ts +46 -0
  113. package/dist/types/src/status.d.ts +33 -0
  114. package/dist/types/src/storage.d.ts +12 -0
  115. package/dist/types/src/store-html.d.ts +39 -0
  116. package/dist/types/src/storefront/catalog.d.ts +38 -0
  117. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  118. package/dist/types/src/variant-render.d.ts +14 -0
  119. package/package.json +24 -24
  120. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  121. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  122. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  123. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  124. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  125. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  126. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  127. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  128. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  129. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  130. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  131. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  132. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  133. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  134. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  135. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  136. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  137. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  138. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  139. package/src/account/addresses-view.js +0 -92
  140. package/src/account/browser-session.js +0 -359
  141. package/src/account/markup.js +0 -48
  142. package/src/account/operations.js +0 -180
  143. package/src/account/order-lines.js +0 -96
  144. package/src/account/order-list.js +0 -91
  145. package/src/account/order-page.js +0 -192
  146. package/src/account/page-events.js +0 -288
  147. package/src/account/server-client.js +0 -172
  148. package/src/account/shared.js +0 -111
  149. package/src/account/shopify-auth.js +0 -150
  150. package/src/account/subscriptions-view.js +0 -158
  151. package/src/account-api.js +0 -118
  152. package/src/account-links.js +0 -35
  153. package/src/account-pages.js +0 -89
  154. package/src/account.js +0 -342
  155. package/src/amounts.js +0 -36
  156. package/src/analytics-shopify.js +0 -241
  157. package/src/analytics.js +0 -352
  158. package/src/api-version.js +0 -85
  159. package/src/astro.js +0 -266
  160. package/src/cart/checkout.js +0 -97
  161. package/src/cart/drawer.js +0 -138
  162. package/src/cart/mutations.js +0 -234
  163. package/src/cart/render.js +0 -294
  164. package/src/cart/store.js +0 -326
  165. package/src/cart.js +0 -391
  166. package/src/client.js +0 -267
  167. package/src/collection.js +0 -183
  168. package/src/config.js +0 -349
  169. package/src/controls/cart-controls.js +0 -155
  170. package/src/controls/discount.js +0 -96
  171. package/src/controls/extras.js +0 -63
  172. package/src/controls/navigation.js +0 -83
  173. package/src/diagnose-markup.js +0 -333
  174. package/src/diagnose.js +0 -295
  175. package/src/dom.js +0 -287
  176. package/src/extras.js +0 -145
  177. package/src/fragments.js +0 -237
  178. package/src/images.js +0 -104
  179. package/src/index.js +0 -353
  180. package/src/initial.js +0 -87
  181. package/src/lazy-iife.js +0 -116
  182. package/src/lazy.js +0 -39
  183. package/src/loader.js +0 -337
  184. package/src/market-codes.js +0 -35
  185. package/src/markets-ui.js +0 -204
  186. package/src/markets.js +0 -334
  187. package/src/markup.js +0 -64
  188. package/src/mounting.js +0 -277
  189. package/src/page-market.js +0 -116
  190. package/src/parts/account-pages.js +0 -6
  191. package/src/parts/account.js +0 -6
  192. package/src/parts/analytics.js +0 -6
  193. package/src/parts/diagnose.js +0 -6
  194. package/src/parts/extras.js +0 -6
  195. package/src/parts/markets.js +0 -6
  196. package/src/parts/search.js +0 -6
  197. package/src/presets/barba.js +0 -190
  198. package/src/product-events.js +0 -130
  199. package/src/product-render.js +0 -257
  200. package/src/product-state.js +0 -318
  201. package/src/product.js +0 -344
  202. package/src/repeaters.js +0 -173
  203. package/src/search.js +0 -323
  204. package/src/selling-plans.js +0 -154
  205. package/src/server.js +0 -215
  206. package/src/shopify-values.js +0 -89
  207. package/src/status.js +0 -126
  208. package/src/store-html.js +0 -0
  209. package/src/storefront/catalog.js +0 -199
  210. package/src/storefront/json-ld.js +0 -167
  211. package/src/variant-render.js +0 -165
  212. package/types/astro.d.ts +0 -28
  213. package/types/barba.d.ts +0 -16
  214. package/types/index.d.ts +0 -577
  215. package/types/server.d.ts +0 -182
@@ -1,96 +0,0 @@
1
- /**
2
- * The discount code form (`tether-discount-form` with `tether-discount-input`) and the remove buttons of applied codes
3
- * (`tether-discount-remove` inside `tether-discount-code`), delegated from the document. A code Shopify refuses marks
4
- * the form `is-invalid` (and the input `aria-invalid`) until the visitor types again.
5
- */
6
- import { reportError } from '../config.js';
7
- import { TetherError } from '../client.js';
8
- import { Cart } from '../cart.js';
9
- import { attr, sel, busy, emit } from '../dom.js';
10
-
11
- /**
12
- * @param {EventTarget | null} target
13
- * @returns {target is Element}
14
- */
15
- const isElement = (target) => target instanceof window.Element;
16
-
17
- /**
18
- * @param {Element} form
19
- * @returns {HTMLInputElement | null}
20
- */
21
- function codeInputOf(form) {
22
- const input = form.querySelector(sel('discount-input')) ?? form.querySelector('input');
23
- return input instanceof window.HTMLInputElement ? input : null;
24
- }
25
-
26
- /**
27
- * @param {Element} form
28
- * @param {HTMLInputElement | null} input
29
- * @param {boolean} invalid
30
- */
31
- function markInvalid(form, input, invalid) {
32
- form.classList.toggle('is-invalid', invalid);
33
- if (invalid) input?.setAttribute('aria-invalid', 'true');
34
- else input?.removeAttribute('aria-invalid');
35
- }
36
-
37
- /**
38
- * @param {Element} form
39
- * @param {HTMLInputElement | null} input
40
- * @param {string} code
41
- */
42
- async function applyCode(form, input, code) {
43
- try {
44
- await Cart.applyDiscount(code);
45
- markInvalid(form, input, false);
46
- emit('discount-applied', { code }, form);
47
- if (input) input.value = '';
48
- } catch (error) {
49
- markInvalid(form, input, true);
50
- if (error instanceof TetherError && error.code === 'INVALID_DISCOUNT') emit('discount-invalid', { code }, form);
51
- else reportError(error, 'discount');
52
- }
53
- }
54
-
55
- /** @param {SubmitEvent} event */
56
- function onSubmit(event) {
57
- const { target } = event;
58
- const form = isElement(target) ? target.closest(sel('discount-form')) : null;
59
- if (!form) return;
60
- event.preventDefault();
61
- const input = codeInputOf(form);
62
- const code = input?.value.trim();
63
- if (!code) return;
64
- void busy(form, () => applyCode(form, input, code));
65
- }
66
-
67
- /** @param {Event} event */
68
- function onInput(event) {
69
- const { target } = event;
70
- if (!isElement(target)) return;
71
- const form = target.closest(sel('discount-form'));
72
- if (!form) return;
73
- form.classList.remove('is-invalid');
74
- target.removeAttribute('aria-invalid');
75
- }
76
-
77
- /** @param {MouseEvent} event */
78
- function onClick(event) {
79
- const { target } = event;
80
- const remove = isElement(target) ? target.closest(sel('discount-remove')) : null;
81
- if (!remove) return;
82
- event.preventDefault();
83
- const code = remove.closest(sel('discount-code'))?.getAttribute(attr('discount-code'));
84
- if (!code) return;
85
- Cart.removeDiscount(code).catch((/** @type {unknown} */ error) => {
86
- reportError(error, 'discount');
87
- });
88
- }
89
-
90
- /** Binds the document-wide listeners, once per page load of the script. */
91
- export function bindDiscountControls() {
92
- // Capture: the submit is handled before a router (Astro's ClientRouter) turns it into a navigation.
93
- document.addEventListener('submit', onSubmit, true);
94
- document.addEventListener('input', onInput);
95
- document.addEventListener('click', onClick);
96
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * The document-wide listeners whose work is in the optional extras part, loaded when one of them first has work:
3
- * group add buttons, the cart or market changing in another tab, and shareable variant links. A part that can't load
4
- * (a self-hosted site without `tether.extras.min.js`) is reported, so the missing file shows.
5
- */
6
- import { cfg, reportError } from '../config.js';
7
- import { attr, sel, busy } from '../dom.js';
8
- import { loadPart } from '../lazy.js';
9
- /** @import { ShopifyCart } from '../cart/store.js' */
10
-
11
- /** @typedef {Parameters<typeof import('../extras.js').writeVariantUrl>[1]} VariantChoice */
12
-
13
- /**
14
- * `[tether-group]` with `[tether-group-add]`: adds every product scope inside the group in one request.
15
- *
16
- * @param {Element} button
17
- * @returns {Promise<ShopifyCart | null>}
18
- */
19
- export const addGroup = (button) => loadPart('extras').then((part) => part.addGroup(button));
20
-
21
- /** @param {unknown} error */
22
- const reportExtrasError = (error) => {
23
- reportError(error, 'extras');
24
- };
25
-
26
- /** @param {MouseEvent} event */
27
- function onClick(event) {
28
- const { target } = event;
29
- const button = target instanceof window.Element ? target.closest(sel('group-add')) : null;
30
- if (!button) return;
31
- event.preventDefault();
32
- busy(button, () => addGroup(button)).catch(reportExtrasError);
33
- }
34
-
35
- /** @param {StorageEvent} event */
36
- function onStorage(event) {
37
- if (!cfg.domain || !event.key?.startsWith(`${cfg.prefix}:`)) return;
38
- loadPart('extras')
39
- .then((part) => {
40
- part.onStorage(event);
41
- })
42
- .catch(reportExtrasError);
43
- }
44
-
45
- /** @param {CustomEvent<VariantChoice>} event `tether:variant-changed`, which a product scope emits. */
46
- function onVariantChanged(event) {
47
- const { target } = event;
48
- if (!(target instanceof window.Element)) return;
49
- if (!cfg.variantUrl && !target.hasAttribute(attr('variant-url'))) return;
50
- loadPart('extras')
51
- .then((part) => {
52
- part.writeVariantUrl(target, event.detail);
53
- })
54
- .catch(reportExtrasError);
55
- }
56
-
57
- /** Binds the document-wide listeners, once per page load of the script. */
58
- export function bindExtrasControls() {
59
- document.addEventListener('click', onClick);
60
- window.addEventListener('storage', onStorage);
61
- // Only product scopes emit this event, always with a CustomEvent.
62
- document.addEventListener(`${cfg.prefix}:variant-changed`, /** @type {EventListener} */ (onVariantChanged));
63
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * How the page follows the visitor across pages and markets: Astro's ClientRouter (`astro:page-load`,
3
- * `astro:before-swap`) mounts each new page in its market and closes the cart while leaving; a country select
4
- * switches the market; back online, what failed to load loads again.
5
- */
6
- import { cfg, reportError } from '../config.js';
7
- import { attr, sel } from '../dom.js';
8
- import { liveLists, retryFailed } from '../mounting.js';
9
- import { marketForArrival, setCountry } from '../page-market.js';
10
-
11
- /**
12
- * @typedef {object} PageHooks What the entry module does for a page.
13
- * @property {() => Promise<unknown>} ready Settles once `init()` has applied the visitor's market.
14
- * @property {(root: Document) => unknown} mount
15
- * @property {() => void} unmount
16
- */
17
-
18
- /**
19
- * The body the last full-document mount ran on: Astro's first `astro:page-load` comes after that mount.
20
- *
21
- * @type {HTMLElement | null} */
22
- let mountedBody = null;
23
-
24
- /** Records a mount of the whole document, so the `astro:page-load` of the same page sends no second round of requests. */
25
- export function rememberDocumentMount() {
26
- mountedBody = document.body;
27
- }
28
-
29
- /** @param {PageHooks} hooks */
30
- async function onPageLoad(hooks) {
31
- await hooks.ready();
32
- const target = marketForArrival();
33
- if (!target) {
34
- if (mountedBody !== document.body) hooks.mount(document);
35
- return;
36
- }
37
- try {
38
- await setCountry(target.country, { language: target.language ?? null, locale: target.locale, remember: false });
39
- } catch (error) {
40
- reportError(error, 'country');
41
- }
42
- hooks.mount(document);
43
- }
44
-
45
- /** @param {PageHooks} hooks */
46
- function bindAstroRouter(hooks) {
47
- document.addEventListener('astro:page-load', () => {
48
- void onPageLoad(hooks);
49
- });
50
- document.addEventListener('astro:before-swap', () => {
51
- mountedBody = null;
52
- hooks.unmount();
53
- });
54
- }
55
-
56
- /** @param {Event} event */
57
- function onChange(event) {
58
- const { target } = event;
59
- if (!(target instanceof window.HTMLSelectElement) || !target.matches(sel('country-select'))) return;
60
- if (!target.value) return;
61
- setCountry(target.value).catch((/** @type {unknown} */ error) => {
62
- reportError(error, 'country');
63
- });
64
- }
65
-
66
- /** Loads the failed products and lists again. */
67
- function onOnline() {
68
- retryFailed();
69
- liveLists.forEach((list) => {
70
- if (list.root.getAttribute(attr('state')) === 'error') void list.load();
71
- });
72
- }
73
-
74
- /**
75
- * Binds the document-wide listeners, once per page load of the script.
76
- *
77
- * @param {PageHooks} hooks
78
- */
79
- export function bindNavigation(hooks) {
80
- if (cfg.astro) bindAstroRouter(hooks);
81
- document.addEventListener('change', onChange);
82
- window.addEventListener('online', onOnline);
83
- }
@@ -1,333 +0,0 @@
1
- /**
2
- * The markup half of `Tether.diagnose()`: problems that show without the network. It knows every value and attribute
3
- * name Tether reads, so a typo gets a "did you mean".
4
- */
5
- import { cfg } from './config.js';
6
- import { attr, sel, get, qsa, hasTemplate } from './dom.js';
7
- import { resolveProductKey, boundary } from './status.js';
8
-
9
- /** @typedef {'error' | 'warn'} ProblemLevel */
10
- /**
11
- * @typedef {object} Problem
12
- * @property {ProblemLevel} level
13
- * @property {string} message
14
- * @property {string} [element] The element, described for people: `<div#id.class>`.
15
- * @property {Element} [el] The element itself, for the console.
16
- */
17
- /** @typedef {(level: ProblemLevel, message: string, el?: Element) => void} AddProblem */
18
-
19
- /** Every `tether-show` value. */
20
- const SHOW = new Set([
21
- 'in-stock',
22
- 'out-of-stock',
23
- 'backorder',
24
- 'low-stock',
25
- 'plenty-in-stock',
26
- 'on-sale',
27
- 'not-on-sale',
28
- 'has-variants',
29
- 'single-variant',
30
- 'subscription-available',
31
- 'subscription-unavailable',
32
- 'subscription-only',
33
- 'one-time-available',
34
- 'subscription-selected',
35
- 'one-time-selected',
36
- 'has-savings',
37
- 'loading',
38
- 'ready',
39
- 'error',
40
- 'not-found',
41
- 'cart-empty',
42
- 'cart-filled',
43
- 'cart-has-subscription',
44
- 'cart-no-subscription',
45
- 'cart-has-discount',
46
- 'cart-error',
47
- 'cart-warning',
48
- 'list-empty',
49
- 'list-filled',
50
- 'search-empty',
51
- 'search-results',
52
- 'logged-in',
53
- 'logged-out',
54
- 'orders-empty',
55
- 'orders-loading',
56
- 'addresses-loading',
57
- 'addresses-empty',
58
- 'addresses-error',
59
- 'subscriptions-loading',
60
- 'subscriptions-empty',
61
- 'subscriptions-error',
62
- ]);
63
- /** Every `tether-bind` value, apart from `image:<n>` and `metafield:<namespace.key>`. */
64
- const BIND = new Set([
65
- 'title',
66
- 'description',
67
- 'description-text',
68
- 'vendor',
69
- 'type',
70
- 'handle',
71
- 'published-at',
72
- 'price',
73
- 'total-price',
74
- 'compare-at-price',
75
- 'unit-price',
76
- 'min-price',
77
- 'max-price',
78
- 'discount-percentage',
79
- 'discount-amount',
80
- 'variant-title',
81
- 'variant-id',
82
- 'sku',
83
- 'barcode',
84
- 'stock',
85
- 'quantity',
86
- 'featured-image',
87
- 'variant-image',
88
- 'images',
89
- 'tags',
90
- 'link',
91
- 'url',
92
- 'one-time-price',
93
- 'subscription-price',
94
- 'subscription-savings',
95
- 'subscription-savings-amount',
96
- 'checkout-charge',
97
- 'plan-name',
98
- 'plan-description',
99
- 'plan-group',
100
- 'option-value',
101
- 'swatch',
102
- 'image',
103
- ]);
104
- /** Every attribute name Tether reads, without the prefix. */
105
- const ATTRIBUTES = new Set(
106
- `account account-bind account-link account-login account-logout add-label add-to-cart adding-text
107
- always-enabled bind cart-checkout-charge cart-clear cart-close cart-count cart-discounts cart-drawer cart-label cart-lines
108
- cart-note cart-open cart-subtotal cart-total cart-warning checkout code collection collection-bind countries country
109
- country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
110
- discount-remove display error group group-add hide-single language limit line line-attribute line-attributes line-id
111
- line-minus line-plan line-plus line-remove line-show list load-more market-pending one-time-label option option-list
112
- option-selected option-value order order-bind order-line order-lines order-line-show order-show order-state order-fulfillments
113
- fulfillment-bind order-return return-quantity return-reason return-note orders profile-form address-form addresses address-bind
114
- address-show address-edit address-new address-delete address-default subscriptions subscription-bind subscription-lines
115
- subscription-line subscription-show subscription-action subscription-status subscription-state confirm form-state form-error page-country page-language page-locale page-title plan plan-bind plan-list
116
- plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
117
- search-bind search-collections search-input search-limit search-min search-products search-queries search-results
118
- search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
119
- sig label hydrated busy`
120
- .split(/\s+/)
121
- .filter(Boolean),
122
- );
123
- /** Attributes that only work inside a `tether-product`. */
124
- const PRODUCT_ONLY = [
125
- 'bind',
126
- 'option-list',
127
- 'option',
128
- 'variant-select',
129
- 'add-to-cart',
130
- 'purchase',
131
- 'plan-list',
132
- 'plan',
133
- 'qty',
134
- 'line-attribute',
135
- ];
136
- const REPEATERS = ['option-list', 'plan-list', 'cart-lines', 'list'];
137
- const METAFIELD_BIND = 'metafield:';
138
- const IMAGE_BIND = /^image:\d+$/;
139
- /** The class names of an element that its description shows. */
140
- const DESCRIBED_CLASSES = 2;
141
-
142
- /**
143
- * @param {string} from
144
- * @param {string} to
145
- * @returns {number} The Levenshtein distance: the fewest single-character edits from one to the other.
146
- */
147
- function editDistance(from, to) {
148
- /** @type {number[]} */
149
- let previous = Array.from({ length: to.length + 1 }, (_, index) => index);
150
- for (let i = 1; i <= from.length; i++) {
151
- const row = [i];
152
- for (let j = 1; j <= to.length; j++) {
153
- const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
154
- row.push(Math.min((previous[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, substitution));
155
- }
156
- previous = row;
157
- }
158
- return previous[to.length] ?? 0;
159
- }
160
-
161
- /**
162
- * @param {string} value What the markup says.
163
- * @param {Iterable<string>} options What Tether reads.
164
- * @param {string} [prefix] Put before the suggestion, e.g. `tether-`.
165
- * @returns {string} ` Did you mean "…"?` for an option close enough to `value`, else `''`.
166
- */
167
- export function didYouMean(value, options, prefix = '') {
168
- /** @type {string | null} */
169
- let best = null;
170
- let bestDistance = Infinity;
171
- for (const option of options) {
172
- const distance = editDistance(value, option);
173
- if (distance < bestDistance) {
174
- bestDistance = distance;
175
- best = option;
176
- }
177
- }
178
- const closeEnough = Math.max(2, Math.floor(value.length / 4));
179
- return best !== null && bestDistance <= closeEnough ? ` Did you mean "${prefix}${best}"?` : '';
180
- }
181
-
182
- /**
183
- * @param {Element} el
184
- * @returns {string} `<div#id.first.second>`.
185
- */
186
- function describe(el) {
187
- const id = el.id ? `#${el.id}` : '';
188
- const classes = el.className && typeof el.className === 'string' ? el.className.trim().split(/\s+/) : [];
189
- const classText = classes.length ? `.${classes.slice(0, DESCRIBED_CLASSES).join('.')}` : '';
190
- return `<${el.tagName.toLowerCase()}${id}${classText}>`;
191
- }
192
-
193
- const listSelector = () => `${sel('collection')},${sel('products')},${sel('recommendations')}`;
194
-
195
- /**
196
- * @param {ParentNode} root
197
- * @param {AddProblem} add
198
- */
199
- function checkProductKeys(root, add) {
200
- qsa(root, sel('product')).forEach((el) => {
201
- // A list card gets its handle when the list renders.
202
- if (el.closest(listSelector()) && el.closest(sel('list'))) return;
203
- const { key, fromUrl, raw } = resolveProductKey(el);
204
- if (!raw) {
205
- add(
206
- 'error',
207
- `${attr('product')} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`,
208
- el,
209
- );
210
- } else if (fromUrl && !key) {
211
- add('warn', `${attr('product')}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
212
- }
213
- });
214
- }
215
-
216
- /**
217
- * @param {ParentNode} root
218
- * @param {AddProblem} add
219
- */
220
- function checkPlacement(root, add) {
221
- for (const name of PRODUCT_ONLY) {
222
- qsa(root, sel(name)).forEach((el) => {
223
- if (el.closest(sel('product'))) return;
224
- if (name === 'line-attribute' && el.closest(sel('group'))) return;
225
- add('warn', `${attr(name)} outside any ${attr('product')}: it does nothing here.`, el);
226
- });
227
- }
228
- qsa(root, sel('group-add')).forEach((el) => {
229
- if (!el.closest(sel('group'))) add('warn', `${attr('group-add')} outside a ${attr('group')}.`, el);
230
- });
231
- for (const name of REPEATERS) {
232
- qsa(root, sel(name)).forEach((el) => {
233
- if (!el.firstElementChild && !hasTemplate(el)) {
234
- add('warn', `${attr(name)} without a template child: put one example element inside it.`, el);
235
- }
236
- });
237
- }
238
- qsa(root, listSelector()).forEach((el) => {
239
- if (!qsa(el, sel('list')).length) {
240
- add('error', `List without a ${attr('list')} child: products have nowhere to render.`, el);
241
- }
242
- });
243
- }
244
-
245
- /**
246
- * @param {ParentNode} root
247
- * @param {AddProblem} add
248
- */
249
- function checkValues(root, add) {
250
- qsa(root, sel('show')).forEach((el) => {
251
- const value = get(el, 'show') ?? '';
252
- if (!SHOW.has(value)) add('warn', `Unknown ${attr('show')}="${value}".${didYouMean(value, SHOW)}`, el);
253
- });
254
- qsa(root, sel('bind')).forEach((el) => {
255
- const value = get(el, 'bind') || '';
256
- if (!value || BIND.has(value) || IMAGE_BIND.test(value) || value.startsWith(METAFIELD_BIND)) return;
257
- add('warn', `Unknown ${attr('bind')}="${value}": it stays empty.${didYouMean(value, BIND)}`, el);
258
- });
259
- }
260
-
261
- /**
262
- * @param {ParentNode} root
263
- * @param {AddProblem} add
264
- */
265
- function checkMetafieldBinds(root, add) {
266
- qsa(root, sel('bind')).forEach((el) => {
267
- const value = get(el, 'bind') || '';
268
- if (!value.startsWith(METAFIELD_BIND)) return;
269
- const ref = value.slice(METAFIELD_BIND.length);
270
- const listed = cfg.metafields.some(
271
- (metafield) => (typeof metafield === 'string' ? metafield : `${metafield.namespace}.${metafield.key}`) === ref,
272
- );
273
- if (!listed) {
274
- add('error', `${attr('bind')}="${value}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
275
- }
276
- });
277
- }
278
-
279
- /**
280
- * Typos in attribute names (tether-bnd, tether-add-to-card) do nothing, silently. Each unknown name is reported once.
281
- *
282
- * @param {ParentNode} root
283
- * @param {AddProblem} add
284
- */
285
- function checkAttributeNames(root, add) {
286
- const prefix = `${cfg.prefix}-`;
287
- /** @type {Set<string>} */
288
- const reported = new Set();
289
- qsa(root, '*').forEach((el) => {
290
- for (const { name: fullName } of el.attributes) {
291
- if (!fullName.startsWith(prefix)) continue;
292
- const name = fullName.slice(prefix.length);
293
- if (ATTRIBUTES.has(name) || reported.has(name)) continue;
294
- reported.add(name);
295
- add('warn', `Unknown attribute ${fullName}: Tether ignores it.${didYouMean(name, ATTRIBUTES, prefix)}`, el);
296
- }
297
- });
298
- }
299
-
300
- /**
301
- * @param {ParentNode} root
302
- * @param {AddProblem} add
303
- */
304
- function checkCartAndScopes(root, add) {
305
- const drawers = qsa(root, sel('cart-drawer')).length;
306
- if (qsa(root, sel('cart-open')).length && !drawers) {
307
- add('warn', `${attr('cart-open')} exists but there is no ${attr('cart-drawer')} on this page.`);
308
- }
309
- if (drawers > 1) add('warn', `More than one ${attr('cart-drawer')}.`);
310
- qsa(root, `[${attr('state')}="error"]`).forEach((el) => {
311
- if (el.matches(boundary())) add('error', `Scope failed to load (${get(el, 'error') || 'error'}).`, el);
312
- });
313
- }
314
-
315
- /**
316
- * @param {ParentNode} [root]
317
- * @returns {Problem[]} The markup problems in `root`, in page order per check.
318
- */
319
- export function checkMarkup(root = document) {
320
- /** @type {Problem[]} */
321
- const problems = [];
322
- /** @type {AddProblem} */
323
- const add = (level, message, el) => {
324
- problems.push(el ? { level, message, element: describe(el), el } : { level, message });
325
- };
326
- checkProductKeys(root, add);
327
- checkPlacement(root, add);
328
- checkValues(root, add);
329
- checkAttributeNames(root, add);
330
- checkMetafieldBinds(root, add);
331
- checkCartAndScopes(root, add);
332
- return problems;
333
- }