@occluze/tether 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) 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 -2
  24. package/dist/presets/barba.esm.js +19 -23
  25. package/dist/tether.account-pages.js +320 -365
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +242 -491
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +66 -88
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +36 -33
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +28 -36
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +1270 -894
  36. package/dist/tether.markets.js +73 -90
  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 +44 -68
  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 +24 -23
  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.js → order-lines.ts} +20 -41
  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.js → page-events.ts} +55 -90
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/{account-pages.js → account-pages.ts} +29 -37
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
  135. package/src/analytics.ts +315 -0
  136. package/src/{api-version.js → api-version.ts} +25 -31
  137. package/src/{astro.js → astro.ts} +110 -104
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/{drawer.js → drawer.ts} +18 -31
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/{render.js → render.ts} +68 -96
  143. package/src/cart/store.ts +279 -0
  144. package/src/{cart.js → cart.ts} +82 -128
  145. package/src/client.ts +391 -0
  146. package/src/{collection.js → collection.ts} +30 -38
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
  150. package/src/controls/{discount.js → discount.ts} +13 -34
  151. package/src/controls/{extras.js → extras.ts} +15 -24
  152. package/src/controls/{navigation.js → navigation.ts} +24 -30
  153. package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
  154. package/src/{diagnose.js → diagnose.ts} +95 -96
  155. package/src/dom.ts +268 -0
  156. package/src/{extras.js → extras.ts} +43 -52
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/{images.js → images.ts} +30 -43
  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.js → loader.ts} +149 -123
  165. package/src/market-codes.ts +6 -0
  166. package/src/{markets-ui.js → markets-ui.ts} +91 -68
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/{mounting.js → mounting.ts} +88 -101
  170. package/src/{page-market.js → page-market.ts} +32 -38
  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.js → product-events.ts} +33 -48
  181. package/src/{product-render.js → product-render.ts} +38 -77
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/{product.js → product.ts} +90 -126
  185. package/src/{repeaters.js → repeaters.ts} +40 -52
  186. package/src/{search.js → search.ts} +85 -103
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/{shopify-values.js → shopify-values.ts} +24 -22
  190. package/src/{status.js → status.ts} +34 -54
  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.js → json-ld.ts} +45 -68
  195. package/src/{variant-render.js → variant-render.ts} +24 -50
  196. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  197. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  198. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  199. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  200. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  201. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  202. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  203. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  204. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  205. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  206. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  207. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  208. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  209. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  210. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  211. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  212. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  213. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  214. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  215. package/src/account/addresses-view.js +0 -92
  216. package/src/account/browser-session.js +0 -359
  217. package/src/account/markup.js +0 -48
  218. package/src/account/operations.js +0 -180
  219. package/src/account/order-list.js +0 -91
  220. package/src/account/order-page.js +0 -192
  221. package/src/account/server-client.js +0 -172
  222. package/src/account/shared.js +0 -111
  223. package/src/account/shopify-auth.js +0 -150
  224. package/src/account-api.js +0 -118
  225. package/src/account-links.js +0 -35
  226. package/src/account.js +0 -342
  227. package/src/amounts.js +0 -36
  228. package/src/analytics.js +0 -352
  229. package/src/cart/checkout.js +0 -97
  230. package/src/cart/mutations.js +0 -234
  231. package/src/cart/store.js +0 -326
  232. package/src/client.js +0 -267
  233. package/src/config.js +0 -349
  234. package/src/dom.js +0 -287
  235. package/src/fragments.js +0 -237
  236. package/src/index.js +0 -353
  237. package/src/initial.js +0 -87
  238. package/src/lazy-iife.js +0 -116
  239. package/src/lazy.js +0 -39
  240. package/src/market-codes.js +0 -35
  241. package/src/markets.js +0 -334
  242. package/src/markup.js +0 -64
  243. package/src/parts/account-pages.js +0 -6
  244. package/src/parts/account.js +0 -6
  245. package/src/parts/analytics.js +0 -6
  246. package/src/parts/diagnose.js +0 -6
  247. package/src/parts/extras.js +0 -6
  248. package/src/parts/markets.js +0 -6
  249. package/src/parts/search.js +0 -6
  250. package/src/presets/barba.js +0 -190
  251. package/src/product-state.js +0 -318
  252. package/src/selling-plans.js +0 -154
  253. package/src/server.js +0 -215
  254. package/src/store-html.js +0 -0
  255. package/src/storefront/catalog.js +0 -199
  256. package/types/astro.d.ts +0 -28
  257. package/types/barba.d.ts +0 -16
  258. package/types/index.d.ts +0 -577
  259. package/types/server.d.ts +0 -182
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ProductScope: everything inside [tether-product="<handle>"] binds to one Shopify product: options, variants,
3
3
  * quantity, subscriptions and add to cart. The scope holds the visitor's choice and the actions on it;
4
- * `product-state.js` derives what the choice shows, `product-render.js` and `variant-render.js` write it, and
5
- * `product-events.js` turns clicks and changes into actions.
4
+ * `product-state.ts` derives what the choice shows, `product-render.ts` and `variant-render.ts` write it, and
5
+ * `product-events.ts` turns clicks and changes into actions.
6
6
  */
7
- import { cfg } from './config.js';
7
+ import { cfg } from './config.ts';
8
8
  import {
9
9
  attr,
10
10
  sel,
@@ -16,41 +16,45 @@ import {
16
16
  notifyLayoutChanged,
17
17
  watchImages,
18
18
  parseJSONAttr,
19
- toAttributes,
20
- } from './dom.js';
21
- import { Cart } from './cart.js';
22
- import { allPlans, allocationFor, findPlan, GID } from './selling-plans.js';
23
- import { initialChoice, normalisePurchase, productView, selectionOf } from './product-state.js';
24
- import { writeProduct } from './product-render.js';
25
- import { writeVariant } from './variant-render.js';
26
- import { bindProductEvents } from './product-events.js';
27
- /** @import { Product, ProductVariant } from './fragments.js' */
28
- /** @import { PlanInGroup } from './selling-plans.js' */
29
- /** @import { Purchase, Selection } from './product-state.js' */
19
+ attributesFromJson,
20
+ } from './dom.ts';
21
+ import { Cart } from './cart.ts';
22
+ import { allPlans, allocationFor, findPlan, GID } from './selling-plans.ts';
23
+ import {
24
+ initialChoice,
25
+ linePurchase,
26
+ normalisePurchase,
27
+ productView,
28
+ purchasePlanId,
29
+ selectionOf,
30
+ } from './product-state.ts';
31
+ import { writeProduct } from './product-render.ts';
32
+ import { writeVariant } from './variant-render.ts';
33
+ import { bindProductEvents } from './product-events.ts';
34
+ import type { Product, ProductVariant } from './fragments.ts';
35
+ import type { ShopifyCart } from './cart/store.ts';
36
+ import type { PlanInGroup } from './selling-plans.ts';
37
+ import type { Purchase, Selection } from './product-state.ts';
30
38
 
31
- /**
32
- * @typedef {object} CartLineInput What a scope adds to the cart.
33
- * @property {string} merchandiseId
34
- * @property {number} quantity
35
- * @property {string} [sellingPlanId]
36
- * @property {{ key: string, value: string }[]} [attributes]
37
- */
39
+ /** What a scope adds to the cart. */
40
+ interface CartLineInput {
41
+ merchandiseId: string;
42
+ quantity: number;
43
+ sellingPlanId?: string;
44
+ attributes?: { key: string; value: string }[];
45
+ }
38
46
 
39
- /** @type {WeakMap<Element, ProductScope>} */
40
- export const scopes = new WeakMap();
47
+ export const scopes = new WeakMap<Element, ProductScope>();
41
48
 
42
49
  const productSelector = () => sel('product');
43
50
 
44
51
  /**
45
52
  * Option names as written in the markup (the build's language), by product handle. In another language Shopify
46
53
  * translates them (Size → Größe), so markup names that aren't the product's are matched by position.
47
- *
48
- * @type {Map<string, string[]>}
49
54
  */
50
- const knownOptionNames = new Map();
55
+ const knownOptionNames = new Map<string, string[]>();
51
56
 
52
- /** @param {readonly Pick<Product, 'handle' | 'options'>[]} products */
53
- export function rememberOptionNames(products) {
57
+ export function rememberOptionNames(products: readonly Pick<Product, 'handle' | 'options'>[]): void {
54
58
  products.forEach((product) => {
55
59
  if (!knownOptionNames.has(product.handle)) {
56
60
  knownOptionNames.set(
@@ -61,23 +65,16 @@ export function rememberOptionNames(products) {
61
65
  });
62
66
  }
63
67
 
64
- /**
65
- * @param {Element} el
66
- * @returns {el is HTMLInputElement}
67
- */
68
- const isInput = (el) => el.tagName === 'INPUT';
68
+ const isInput = (el: Element): el is HTMLInputElement => el.tagName === 'INPUT';
69
69
 
70
- /**
71
- * @param {Element} el
72
- * @returns {el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement}
73
- */
74
- const isField = (el) => el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA';
70
+ const isField = (el: Element): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement =>
71
+ el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA';
75
72
 
76
73
  /**
77
- * @param {HTMLElement} el A `tether-line-attribute` field.
78
- * @returns {string | null} Its value for the cart line; `null` for an unchecked or empty field.
74
+ * @param el A `tether-line-attribute` field.
75
+ * @returns Its value for the cart line; `null` for an unchecked or empty field.
79
76
  */
80
- function lineAttributeValue(el) {
77
+ function lineAttributeValue(el: HTMLElement): string | null {
81
78
  if (isInput(el) && el.type === 'checkbox') {
82
79
  if (!el.checked) return null;
83
80
  return el.value && el.value !== 'on' ? el.value : 'Yes';
@@ -87,11 +84,16 @@ function lineAttributeValue(el) {
87
84
  }
88
85
 
89
86
  export class ProductScope {
90
- /**
91
- * @param {HTMLElement} root
92
- * @param {Product} product
93
- */
94
- constructor(root, product) {
87
+ readonly root: HTMLElement;
88
+ product: Product;
89
+ plans: PlanInGroup[];
90
+ selected: Selection;
91
+ /** How the visitor buys; `setPurchase` and `selectPlan` change it. */
92
+ purchase: Purchase;
93
+ variant: ProductVariant | null;
94
+ quantity: number;
95
+
96
+ constructor(root: HTMLElement, product: Product) {
95
97
  this.root = root;
96
98
  this.product = product;
97
99
  this.plans = allPlans(product);
@@ -102,11 +104,8 @@ export class ProductScope {
102
104
  defaultPlan: get(root, 'default-plan'),
103
105
  defaultPurchase: get(root, 'default-purchase') || cfg.defaultPurchase,
104
106
  });
105
- /** @type {Selection} */
106
107
  this.selected = choice.selected;
107
- /** @type {Purchase} */
108
- this.purchaseChoice = choice.purchase;
109
- /** @type {ProductVariant | null} */
108
+ this.purchase = choice.purchase;
110
109
  this.variant = null;
111
110
  this.quantity = has(root, 'quantity') ? Math.max(0, parseInt(get(root, 'quantity') ?? '', 10) || 0) : 1;
112
111
 
@@ -122,12 +121,8 @@ export class ProductScope {
122
121
  emit('product-ready', { product, scope: this }, root);
123
122
  }
124
123
 
125
- /**
126
- * New data for the same product (a country switch). Keeps the visitor's choice.
127
- *
128
- * @param {Product} product
129
- */
130
- update(product) {
124
+ /** New data for the same product (a country switch). Keeps the visitor's choice. */
125
+ update(product: Product): void {
131
126
  const variantId = this.variant?.id;
132
127
  this.product = product;
133
128
  this.plans = allPlans(product);
@@ -141,20 +136,17 @@ export class ProductScope {
141
136
  emit('product-updated', { product, scope: this }, this.root);
142
137
  }
143
138
 
144
- /**
145
- * @param {string} selector
146
- * @returns {HTMLElement[]} Matches in this scope, including the root itself (a card link with tether-bind="link").
147
- */
148
- $(selector) {
139
+ /** @returns Matches in this scope, including the root itself (a card link with tether-bind="link"). */
140
+ $(selector: string): HTMLElement[] {
149
141
  const inner = scoped(this.root, selector, productSelector());
150
142
  return this.root.matches(selector) ? [this.root, ...inner] : inner;
151
143
  }
152
144
 
153
145
  /**
154
- * @param {string | null} authored An option name in the markup.
155
- * @returns {string} The product's option of that name, or at that position in the build's language.
146
+ * @param authored An option name in the markup.
147
+ * @returns The product's option of that name, or at that position in the build's language.
156
148
  */
157
- optName(authored) {
149
+ optName(authored: string | null): string {
158
150
  if (!authored) return '';
159
151
  const { options } = this.product;
160
152
  if (options.some((option) => option.name === authored)) return authored;
@@ -162,57 +154,35 @@ export class ProductScope {
162
154
  return options[index]?.name ?? authored;
163
155
  }
164
156
 
165
- /** @returns {'one-time' | 'subscription'} */
166
- get purchase() {
167
- return this.purchaseChoice.kind;
168
- }
169
-
170
- /** @param {string} kind */
171
- set purchase(kind) {
172
- const wanted = kind === 'subscription' ? 'subscription' : 'one-time';
173
- this.purchaseChoice = normalisePurchase(this.product, this.variant, { kind: wanted, planId: this.planId });
174
- }
175
-
176
- /** @returns {string | null} The plan bought on, or offered while buying once. */
177
- get planId() {
178
- return this.purchaseChoice.planId;
179
- }
180
-
181
- /** @param {string | null} planId */
182
- set planId(planId) {
183
- this.purchaseChoice = normalisePurchase(this.product, this.variant, { kind: this.purchase, planId });
184
- }
185
-
186
- /** @returns {PlanInGroup | null} */
187
- get plan() {
188
- return this.plans.find((plan) => plan.id === this.planId) ?? null;
157
+ get plan(): PlanInGroup | null {
158
+ const planId = purchasePlanId(this.purchase);
159
+ return this.plans.find((plan) => plan.id === planId) ?? null;
189
160
  }
190
161
 
191
162
  /** True when the current variant can be bought on a plan. */
192
- get subscriptionAvailable() {
163
+ get subscriptionAvailable(): boolean {
193
164
  return this.plans.some((plan) => allocationFor(this.variant, plan.id));
194
165
  }
195
166
 
196
- get isSubscription() {
197
- return this.purchase === 'subscription' && allocationFor(this.variant, this.planId) !== null;
167
+ /** True when the line is bought on a plan: subscribing, and the variant is on the plan. */
168
+ get isSubscription(): boolean {
169
+ return linePurchase(this.purchase, this.variant).kind === 'subscription';
198
170
  }
199
171
 
200
- /** @returns {CartLineInput | null} What would be added to the cart right now. */
201
- getLine() {
172
+ /** @returns What would be added to the cart right now. */
173
+ getLine(): CartLineInput | null {
202
174
  if (!this.variant) return null;
203
- /** @type {CartLineInput} */
204
- const line = { merchandiseId: this.variant.id, quantity: this.quantity };
205
- if (this.isSubscription && this.planId) line.sellingPlanId = this.planId;
175
+ const line: CartLineInput = { merchandiseId: this.variant.id, quantity: this.quantity };
176
+ const bought = linePurchase(this.purchase, this.variant);
177
+ if (bought.kind === 'subscription') line.sellingPlanId = bought.planId;
206
178
  const attributes = this.collectAttributes();
207
179
  if (attributes.length) line.attributes = attributes;
208
180
  return line;
209
181
  }
210
182
 
211
- /** @returns {{ key: string, value: string }[]} `tether-line-attributes` and the filled `tether-line-attribute` fields. */
212
- collectAttributes() {
213
- // The site's own JSON: toAttributes writes every value with String(), whatever its type.
214
- const json = /** @type {Parameters<typeof toAttributes>[0]} */ (parseJSONAttr(this.root, 'line-attributes'));
215
- const fixed = toAttributes(json);
183
+ /** @returns `tether-line-attributes` and the filled `tether-line-attribute` fields. */
184
+ collectAttributes(): { key: string; value: string }[] {
185
+ const fixed = attributesFromJson(parseJSONAttr(this.root, 'line-attributes'));
216
186
  const fields = this.$(sel('line-attribute')).flatMap((el) => {
217
187
  const value = lineAttributeValue(el);
218
188
  return value === null ? [] : [{ key: get(el, 'line-attribute') ?? '', value }];
@@ -223,11 +193,8 @@ export class ProductScope {
223
193
  /**
224
194
  * Selects an option value. A combination no variant has moves to the closest variant with the value: the most other
225
195
  * choices kept, then one for sale.
226
- *
227
- * @param {string} name
228
- * @param {string} value
229
196
  */
230
- select(name, value) {
197
+ select(name: string, value: string): void {
231
198
  if (this.selected[name] === value) return;
232
199
  const withValue = this.product.variants.nodes.filter((variant) =>
233
200
  variant.selectedOptions.some((option) => option.name === name && option.value === value),
@@ -237,7 +204,7 @@ export class ProductScope {
237
204
  if (!firstWithValue) return;
238
205
  const next = { ...this.selected, [name]: value };
239
206
  if (!withValue.some((variant) => variant.selectedOptions.every((option) => next[option.name] === option.value))) {
240
- const score = (/** @type {ProductVariant} */ variant) => {
207
+ const score = (variant: ProductVariant) => {
241
208
  const kept = variant.selectedOptions.filter(
242
209
  (option) => option.name !== name && this.selected[option.name] === option.value,
243
210
  ).length;
@@ -254,34 +221,31 @@ export class ProductScope {
254
221
  this.renderVariant();
255
222
  }
256
223
 
257
- /** @param {string} id A variant gid or numeric id. */
258
- selectVariant(id) {
224
+ /** @param id A variant gid or numeric id. */
225
+ selectVariant(id: string): void {
259
226
  const variant = this.product.variants.nodes.find((candidate) => candidate.id === GID.variant(id));
260
227
  if (!variant) return;
261
228
  this.selected = selectionOf(variant);
262
229
  this.renderVariant();
263
230
  }
264
231
 
265
- /** @param {string | null} mode `subscription`, else buying once. */
266
- setPurchase(mode) {
267
- const kind = mode === 'subscription' ? 'subscription' : 'one-time';
268
- const next = normalisePurchase(this.product, this.variant, { kind, planId: this.planId });
232
+ setPurchase(kind: Purchase['kind']): void {
233
+ const next = normalisePurchase(this.product, this.variant, kind, purchasePlanId(this.purchase));
269
234
  // The product decides when the wanted kind isn't possible (no plans, or a plan required).
270
- if (next.kind === this.purchase) return;
271
- this.purchaseChoice = next;
235
+ if (next.kind === this.purchase.kind) return;
236
+ this.purchase = next;
272
237
  this.renderVariant();
273
238
  }
274
239
 
275
- /** @param {string | null} ref A plan id, numeric id, name or option value. */
276
- selectPlan(ref) {
240
+ /** @param ref A plan id, numeric id, name or option value. */
241
+ selectPlan(ref: string | null): void {
277
242
  const plan = findPlan(this.product, ref);
278
243
  if (!plan) return;
279
- this.purchaseChoice = { kind: 'subscription', planId: plan.id };
244
+ this.purchase = { kind: 'subscription', planId: plan.id };
280
245
  this.renderVariant();
281
246
  }
282
247
 
283
- /** @param {number | string} quantity */
284
- setQuantity(quantity) {
248
+ setQuantity(quantity: number | string): void {
285
249
  const minimum = has(this.root, 'quantity') ? 0 : 1;
286
250
  this.quantity = Math.max(minimum, parseInt(String(quantity), 10) || 0);
287
251
  this.$(sel('qty')).forEach((el) => {
@@ -290,8 +254,8 @@ export class ProductScope {
290
254
  this.renderVariant();
291
255
  }
292
256
 
293
- /** @returns {Promise<unknown>} The cart after adding; `null` when the variant can't be added. */
294
- async addToCart() {
257
+ /** @returns The cart after adding; `null` when the variant can't be added. */
258
+ async addToCart(): Promise<ShopifyCart | null> {
295
259
  if (!this.variant?.availableForSale) return null;
296
260
  const line = this.getLine();
297
261
  if (!line || line.quantity < 1) return null;
@@ -301,7 +265,7 @@ export class ProductScope {
301
265
  }
302
266
 
303
267
  /** Learns the markup's option names when they are the product's, then writes the product. */
304
- renderProduct() {
268
+ renderProduct(): void {
305
269
  const { product } = this;
306
270
  if (!knownOptionNames.has(product.handle)) {
307
271
  const authored = this.$(`${sel('option-list')},select${sel('option')}`).map(
@@ -315,10 +279,10 @@ export class ProductScope {
315
279
  }
316
280
 
317
281
  /** Derives the view of the current choice, keeps its variant, selection and purchase, and writes it. */
318
- renderVariant() {
282
+ renderVariant(): void {
319
283
  const view = productView(
320
284
  this.product,
321
- { selected: this.selected, purchase: this.purchaseChoice, quantity: this.quantity },
285
+ { selected: this.selected, purchase: this.purchase, quantity: this.quantity },
322
286
  cfg.lowStock,
323
287
  );
324
288
  if (!view) {
@@ -327,7 +291,7 @@ export class ProductScope {
327
291
  }
328
292
  this.variant = view.variant;
329
293
  this.selected = view.selected;
330
- this.purchaseChoice = view.purchase;
294
+ this.purchase = view.purchase;
331
295
  writeVariant(this, view);
332
296
  emit(
333
297
  'variant-changed',
@@ -336,7 +300,7 @@ export class ProductScope {
336
300
  variant: view.variant,
337
301
  selected: { ...view.selected },
338
302
  purchase: view.purchase.kind,
339
- sellingPlanId: view.isSubscription ? view.purchase.planId : null,
303
+ sellingPlanId: view.line.kind === 'subscription' ? view.line.planId : null,
340
304
  },
341
305
  this.root,
342
306
  );
@@ -2,46 +2,37 @@
2
2
  * Lists rendered from a template: a repeater's first child in the designer is the template, and every item gets a
3
3
  * copy of it. The other children are placeholder content and are removed.
4
4
  */
5
- import { cfg, reportError, warn } from './config.js';
5
+ import { cfg, reportError, warn } from './config.ts';
6
6
 
7
- /** @type {WeakMap<Element, HTMLElement | null>} Each repeater's template, captured on first render. */
8
- const templates = new WeakMap();
7
+ /** Each repeater's template, captured on first render. */
8
+ const templates = new WeakMap<Element, HTMLElement | null>();
9
9
 
10
- /**
11
- * @param {Element} container
12
- * @returns {boolean} True once the repeater's template has been captured (it may be empty after that).
13
- */
14
- export const hasTemplate = (container) => Boolean(templates.get(container));
10
+ /** @returns True once the repeater's template has been captured (it may be empty after that). */
11
+ export const hasTemplate = (container: Element): boolean => Boolean(templates.get(container));
15
12
 
16
13
  /**
17
14
  * A deep copy of a template. `cloneNode` is typed as returning a `Node`; a copy of an element is an element of the
18
15
  * same kind.
19
- *
20
- * @param {HTMLElement} template
21
16
  */
22
- const copyOf = (template) => /** @type {HTMLElement} */ (template.cloneNode(true));
17
+ const copyOf = (template: HTMLElement) => template.cloneNode(true) as HTMLElement;
23
18
 
24
- /** @param {Element} container */
25
- function templateOf(container) {
19
+ function templateOf(container: Element): HTMLElement | null {
26
20
  const known = templates.get(container);
27
21
  if (known !== undefined) return known;
28
22
  const first = container.firstElementChild;
29
23
  // Repeaters are HTML lists in the designer; their first child is an HTML element.
30
- const template = first ? copyOf(/** @type {HTMLElement} */ (first)) : null;
24
+ const template = first ? copyOf(first as HTMLElement) : null;
31
25
  templates.set(container, template);
32
26
  container.replaceChildren();
33
27
  return template;
34
28
  }
35
29
 
36
- /**
37
- * Rebuilds the list on every render. Fine for images, tags, options.
38
- *
39
- * @template Item
40
- * @param {Element} container
41
- * @param {readonly Item[]} items
42
- * @param {(node: HTMLElement, item: Item, index: number) => void} render
43
- */
44
- export function repeat(container, items, render) {
30
+ /** Rebuilds the list on every render. Fine for images, tags, options. */
31
+ export function repeat<Item>(
32
+ container: Element,
33
+ items: readonly Item[],
34
+ render: (node: HTMLElement, item: Item, index: number) => void,
35
+ ): void {
45
36
  const template = templateOf(container);
46
37
  if (!template) {
47
38
  if (items.length) warn('repeater without a template child', container);
@@ -55,18 +46,14 @@ export function repeat(container, items, render) {
55
46
  });
56
47
  }
57
48
 
58
- /**
59
- * @typedef {object} KeyedHooks
60
- * @property {((node: HTMLElement) => void) | null} [onEnter] Runs on a node inserted for a new item.
61
- * @property {((node: HTMLElement, done: () => void) => void) | null} [onLeave] Runs on a node whose item is gone;
62
- * `done()` removes it.
63
- */
49
+ interface KeyedHooks {
50
+ /** Runs on a node inserted for a new item. */
51
+ onEnter?: ((node: HTMLElement) => void) | null;
52
+ /** Runs on a node whose item is gone; `done()` removes it. */
53
+ onLeave?: ((node: HTMLElement, done: () => void) => void) | null;
54
+ }
64
55
 
65
- /**
66
- * @param {HTMLElement} node
67
- * @param {KeyedHooks['onLeave']} onLeave
68
- */
69
- function leave(node, onLeave) {
56
+ function leave(node: HTMLElement, onLeave: KeyedHooks['onLeave']): void {
70
57
  const done = () => {
71
58
  node.remove();
72
59
  };
@@ -85,12 +72,15 @@ function leave(node, onLeave) {
85
72
  /**
86
73
  * Inserts the nodes of new items next to the item before them, leaving the existing nodes where they are.
87
74
  *
88
- * @param {Element} container
89
- * @param {HTMLElement[]} wanted Every item's node, in order.
90
- * @param {Set<HTMLElement>} fresh The nodes not yet in the container.
91
- * @param {KeyedHooks['onEnter']} onEnter
75
+ * @param wanted Every item's node, in order.
76
+ * @param fresh The nodes not yet in the container.
92
77
  */
93
- function insertFresh(container, wanted, fresh, onEnter) {
78
+ function insertFresh(
79
+ container: Element,
80
+ wanted: HTMLElement[],
81
+ fresh: Set<HTMLElement>,
82
+ onEnter: KeyedHooks['onEnter'],
83
+ ): void {
94
84
  wanted.forEach((node, index) => {
95
85
  if (!fresh.has(node)) return;
96
86
  const previous = wanted[index - 1];
@@ -108,15 +98,14 @@ function insertFresh(container, wanted, fresh, onEnter) {
108
98
  /**
109
99
  * Keyed repeater: keeps existing nodes, inserts new ones, removes old ones. Used for cart lines, so values can
110
100
  * animate and lines can slide in and out.
111
- *
112
- * @template Item
113
- * @param {Element} container
114
- * @param {readonly Item[]} items
115
- * @param {(item: Item, index: number) => string | number} keyOf
116
- * @param {(node: HTMLElement, item: Item, index: number, isNew: boolean) => void} render
117
- * @param {KeyedHooks} [hooks]
118
101
  */
119
- export function repeatKeyed(container, items, keyOf, render, { onEnter, onLeave } = {}) {
102
+ export function repeatKeyed<Item>(
103
+ container: Element,
104
+ items: readonly Item[],
105
+ keyOf: (item: Item, index: number) => string | number,
106
+ render: (node: HTMLElement, item: Item, index: number, isNew: boolean) => void,
107
+ { onEnter, onLeave }: KeyedHooks = {},
108
+ ): void {
120
109
  const template = templateOf(container);
121
110
  if (!template) {
122
111
  warn('repeater without a template child', container);
@@ -125,16 +114,15 @@ export function repeatKeyed(container, items, keyOf, render, { onEnter, onLeave
125
114
 
126
115
  const keyAttribute = `data-${cfg.prefix}-key`;
127
116
  const leavingAttribute = `data-${cfg.prefix}-leaving`;
117
+ // The list's children are copies of its template, HTML elements like it.
128
118
  const live = () =>
129
- [.../** @type {HTMLCollectionOf<HTMLElement>} */ (container.children)].filter(
119
+ [...(container.children as HTMLCollectionOf<HTMLElement>)].filter(
130
120
  (node) => node.hasAttribute(keyAttribute) && !node.hasAttribute(leavingAttribute),
131
121
  );
132
122
 
133
123
  const existing = new Map(live().map((node) => [node.getAttribute(keyAttribute), node]));
134
- /** @type {HTMLElement[]} */
135
- const wanted = [];
136
- /** @type {Set<HTMLElement>} */
137
- const fresh = new Set();
124
+ const wanted: HTMLElement[] = [];
125
+ const fresh = new Set<HTMLElement>();
138
126
 
139
127
  items.forEach((item, index) => {
140
128
  const key = String(keyOf(item, index));