@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.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +320 -365
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +242 -491
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +66 -88
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +36 -33
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +28 -36
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1270 -894
- package/dist/tether.markets.js +73 -90
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +44 -68
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -23
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/{order-lines.js → order-lines.ts} +20 -41
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/{page-events.js → page-events.ts} +55 -90
- package/src/account/server-client.ts +183 -0
- package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/{account-pages.js → account-pages.ts} +29 -37
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
- package/src/analytics.ts +315 -0
- package/src/{api-version.js → api-version.ts} +25 -31
- package/src/{astro.js → astro.ts} +110 -104
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/{drawer.js → drawer.ts} +18 -31
- package/src/cart/mutations.ts +237 -0
- package/src/cart/{render.js → render.ts} +68 -96
- package/src/cart/store.ts +279 -0
- package/src/{cart.js → cart.ts} +82 -128
- package/src/client.ts +391 -0
- package/src/{collection.js → collection.ts} +30 -38
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
- package/src/controls/{discount.js → discount.ts} +13 -34
- package/src/controls/{extras.js → extras.ts} +15 -24
- package/src/controls/{navigation.js → navigation.ts} +24 -30
- package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
- package/src/{diagnose.js → diagnose.ts} +95 -96
- package/src/dom.ts +268 -0
- package/src/{extras.js → extras.ts} +43 -52
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/{images.js → images.ts} +30 -43
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/{loader.js → loader.ts} +149 -123
- package/src/market-codes.ts +6 -0
- package/src/{markets-ui.js → markets-ui.ts} +91 -68
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/{mounting.js → mounting.ts} +88 -101
- package/src/{page-market.js → page-market.ts} +32 -38
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/{product-events.js → product-events.ts} +33 -48
- package/src/{product-render.js → product-render.ts} +38 -77
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/{product.js → product.ts} +90 -126
- package/src/{repeaters.js → repeaters.ts} +40 -52
- package/src/{search.js → search.ts} +85 -103
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/{shopify-values.js → shopify-values.ts} +24 -22
- package/src/{status.js → status.ts} +34 -54
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
- package/src/{variant-render.js → variant-render.ts} +24 -50
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics.js +0 -352
- package/src/cart/checkout.js +0 -97
- package/src/cart/mutations.js +0 -234
- package/src/cart/store.js +0 -326
- package/src/client.js +0 -267
- package/src/config.js +0 -349
- package/src/dom.js +0 -287
- package/src/fragments.js +0 -237
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/market-codes.js +0 -35
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-state.js +0 -318
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- 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.
|
|
5
|
-
* `product-events.
|
|
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.
|
|
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
|
-
|
|
20
|
-
} from './dom.
|
|
21
|
-
import { Cart } from './cart.
|
|
22
|
-
import { allPlans, allocationFor, findPlan, GID } from './selling-plans.
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
* @returns
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
* @returns
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
return this.
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
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
|
|
212
|
-
collectAttributes() {
|
|
213
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
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.
|
|
235
|
+
if (next.kind === this.purchase.kind) return;
|
|
236
|
+
this.purchase = next;
|
|
272
237
|
this.renderVariant();
|
|
273
238
|
}
|
|
274
239
|
|
|
275
|
-
/** @param
|
|
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.
|
|
244
|
+
this.purchase = { kind: 'subscription', planId: plan.id };
|
|
280
245
|
this.renderVariant();
|
|
281
246
|
}
|
|
282
247
|
|
|
283
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
5
|
+
import { cfg, reportError, warn } from './config.ts';
|
|
6
6
|
|
|
7
|
-
/**
|
|
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
|
-
|
|
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) =>
|
|
17
|
+
const copyOf = (template: HTMLElement) => template.cloneNode(true) as HTMLElement;
|
|
23
18
|
|
|
24
|
-
|
|
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(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
89
|
-
* @param
|
|
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(
|
|
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(
|
|
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
|
-
[
|
|
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
|
-
|
|
135
|
-
const
|
|
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));
|