@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.
- package/README.md +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.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 +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- 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 +60 -69
- 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/cart-id.d.ts +2 -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 +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -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 +190 -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 +90 -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 -24
- 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-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- 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/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- 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/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- 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-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- 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
package/src/product.js
DELETED
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProductScope: everything inside [tether-product="<handle>"] binds to one Shopify product: options, variants,
|
|
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.
|
|
6
|
-
*/
|
|
7
|
-
import { cfg } from './config.js';
|
|
8
|
-
import {
|
|
9
|
-
attr,
|
|
10
|
-
sel,
|
|
11
|
-
get,
|
|
12
|
-
has,
|
|
13
|
-
scoped,
|
|
14
|
-
emit,
|
|
15
|
-
setText,
|
|
16
|
-
notifyLayoutChanged,
|
|
17
|
-
watchImages,
|
|
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' */
|
|
30
|
-
|
|
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
|
-
*/
|
|
38
|
-
|
|
39
|
-
/** @type {WeakMap<Element, ProductScope>} */
|
|
40
|
-
export const scopes = new WeakMap();
|
|
41
|
-
|
|
42
|
-
const productSelector = () => sel('product');
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Option names as written in the markup (the build's language), by product handle. In another language Shopify
|
|
46
|
-
* 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
|
-
*/
|
|
50
|
-
const knownOptionNames = new Map();
|
|
51
|
-
|
|
52
|
-
/** @param {readonly Pick<Product, 'handle' | 'options'>[]} products */
|
|
53
|
-
export function rememberOptionNames(products) {
|
|
54
|
-
products.forEach((product) => {
|
|
55
|
-
if (!knownOptionNames.has(product.handle)) {
|
|
56
|
-
knownOptionNames.set(
|
|
57
|
-
product.handle,
|
|
58
|
-
product.options.map((option) => option.name),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @param {Element} el
|
|
66
|
-
* @returns {el is HTMLInputElement}
|
|
67
|
-
*/
|
|
68
|
-
const isInput = (el) => el.tagName === 'INPUT';
|
|
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';
|
|
75
|
-
|
|
76
|
-
/**
|
|
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.
|
|
79
|
-
*/
|
|
80
|
-
function lineAttributeValue(el) {
|
|
81
|
-
if (isInput(el) && el.type === 'checkbox') {
|
|
82
|
-
if (!el.checked) return null;
|
|
83
|
-
return el.value && el.value !== 'on' ? el.value : 'Yes';
|
|
84
|
-
}
|
|
85
|
-
if (isInput(el) && el.type === 'radio') return el.checked ? el.value : null;
|
|
86
|
-
return (isField(el) && el.value) || null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export class ProductScope {
|
|
90
|
-
/**
|
|
91
|
-
* @param {HTMLElement} root
|
|
92
|
-
* @param {Product} product
|
|
93
|
-
*/
|
|
94
|
-
constructor(root, product) {
|
|
95
|
-
this.root = root;
|
|
96
|
-
this.product = product;
|
|
97
|
-
this.plans = allPlans(product);
|
|
98
|
-
const params = new URLSearchParams(window.location.search);
|
|
99
|
-
const choice = initialChoice(product, {
|
|
100
|
-
variant: params.get('variant') || get(root, 'variant'),
|
|
101
|
-
sellingPlan: params.get('selling_plan'),
|
|
102
|
-
defaultPlan: get(root, 'default-plan'),
|
|
103
|
-
defaultPurchase: get(root, 'default-purchase') || cfg.defaultPurchase,
|
|
104
|
-
});
|
|
105
|
-
/** @type {Selection} */
|
|
106
|
-
this.selected = choice.selected;
|
|
107
|
-
/** @type {Purchase} */
|
|
108
|
-
this.purchaseChoice = choice.purchase;
|
|
109
|
-
/** @type {ProductVariant | null} */
|
|
110
|
-
this.variant = null;
|
|
111
|
-
this.quantity = has(root, 'quantity') ? Math.max(0, parseInt(get(root, 'quantity') ?? '', 10) || 0) : 1;
|
|
112
|
-
|
|
113
|
-
scopes.set(root, this);
|
|
114
|
-
root.setAttribute(attr('state'), 'ready');
|
|
115
|
-
root.removeAttribute(attr('error'));
|
|
116
|
-
this.renderProduct();
|
|
117
|
-
bindProductEvents(this);
|
|
118
|
-
this.renderVariant();
|
|
119
|
-
|
|
120
|
-
watchImages(root, 'product');
|
|
121
|
-
notifyLayoutChanged('product');
|
|
122
|
-
emit('product-ready', { product, scope: this }, root);
|
|
123
|
-
}
|
|
124
|
-
|
|
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) {
|
|
131
|
-
const variantId = this.variant?.id;
|
|
132
|
-
this.product = product;
|
|
133
|
-
this.plans = allPlans(product);
|
|
134
|
-
const same = product.variants.nodes.find((variant) => variant.id === variantId);
|
|
135
|
-
if (same) this.selected = selectionOf(same);
|
|
136
|
-
this.root.setAttribute(attr('state'), 'ready');
|
|
137
|
-
this.root.removeAttribute(attr('error'));
|
|
138
|
-
this.renderProduct();
|
|
139
|
-
this.renderVariant();
|
|
140
|
-
notifyLayoutChanged('product');
|
|
141
|
-
emit('product-updated', { product, scope: this }, this.root);
|
|
142
|
-
}
|
|
143
|
-
|
|
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) {
|
|
149
|
-
const inner = scoped(this.root, selector, productSelector());
|
|
150
|
-
return this.root.matches(selector) ? [this.root, ...inner] : inner;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
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.
|
|
156
|
-
*/
|
|
157
|
-
optName(authored) {
|
|
158
|
-
if (!authored) return '';
|
|
159
|
-
const { options } = this.product;
|
|
160
|
-
if (options.some((option) => option.name === authored)) return authored;
|
|
161
|
-
const index = (knownOptionNames.get(this.product.handle) ?? []).indexOf(authored);
|
|
162
|
-
return options[index]?.name ?? authored;
|
|
163
|
-
}
|
|
164
|
-
|
|
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;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** True when the current variant can be bought on a plan. */
|
|
192
|
-
get subscriptionAvailable() {
|
|
193
|
-
return this.plans.some((plan) => allocationFor(this.variant, plan.id));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
get isSubscription() {
|
|
197
|
-
return this.purchase === 'subscription' && allocationFor(this.variant, this.planId) !== null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** @returns {CartLineInput | null} What would be added to the cart right now. */
|
|
201
|
-
getLine() {
|
|
202
|
-
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;
|
|
206
|
-
const attributes = this.collectAttributes();
|
|
207
|
-
if (attributes.length) line.attributes = attributes;
|
|
208
|
-
return line;
|
|
209
|
-
}
|
|
210
|
-
|
|
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);
|
|
216
|
-
const fields = this.$(sel('line-attribute')).flatMap((el) => {
|
|
217
|
-
const value = lineAttributeValue(el);
|
|
218
|
-
return value === null ? [] : [{ key: get(el, 'line-attribute') ?? '', value }];
|
|
219
|
-
});
|
|
220
|
-
return [...fixed, ...fields];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Selects an option value. A combination no variant has moves to the closest variant with the value: the most other
|
|
225
|
-
* choices kept, then one for sale.
|
|
226
|
-
*
|
|
227
|
-
* @param {string} name
|
|
228
|
-
* @param {string} value
|
|
229
|
-
*/
|
|
230
|
-
select(name, value) {
|
|
231
|
-
if (this.selected[name] === value) return;
|
|
232
|
-
const withValue = this.product.variants.nodes.filter((variant) =>
|
|
233
|
-
variant.selectedOptions.some((option) => option.name === name && option.value === value),
|
|
234
|
-
);
|
|
235
|
-
const [firstWithValue] = withValue;
|
|
236
|
-
// A value no variant has: nothing to select, the visitor's choice stays.
|
|
237
|
-
if (!firstWithValue) return;
|
|
238
|
-
const next = { ...this.selected, [name]: value };
|
|
239
|
-
if (!withValue.some((variant) => variant.selectedOptions.every((option) => next[option.name] === option.value))) {
|
|
240
|
-
const score = (/** @type {ProductVariant} */ variant) => {
|
|
241
|
-
const kept = variant.selectedOptions.filter(
|
|
242
|
-
(option) => option.name !== name && this.selected[option.name] === option.value,
|
|
243
|
-
).length;
|
|
244
|
-
// One kept choice outweighs being for sale.
|
|
245
|
-
return kept * 2 + (variant.availableForSale ? 1 : 0);
|
|
246
|
-
};
|
|
247
|
-
const closest = withValue.reduce(
|
|
248
|
-
(best, variant) => (score(variant) > score(best) ? variant : best),
|
|
249
|
-
firstWithValue,
|
|
250
|
-
);
|
|
251
|
-
Object.assign(next, selectionOf(closest));
|
|
252
|
-
}
|
|
253
|
-
this.selected = next;
|
|
254
|
-
this.renderVariant();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** @param {string} id A variant gid or numeric id. */
|
|
258
|
-
selectVariant(id) {
|
|
259
|
-
const variant = this.product.variants.nodes.find((candidate) => candidate.id === GID.variant(id));
|
|
260
|
-
if (!variant) return;
|
|
261
|
-
this.selected = selectionOf(variant);
|
|
262
|
-
this.renderVariant();
|
|
263
|
-
}
|
|
264
|
-
|
|
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 });
|
|
269
|
-
// 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;
|
|
272
|
-
this.renderVariant();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** @param {string | null} ref A plan id, numeric id, name or option value. */
|
|
276
|
-
selectPlan(ref) {
|
|
277
|
-
const plan = findPlan(this.product, ref);
|
|
278
|
-
if (!plan) return;
|
|
279
|
-
this.purchaseChoice = { kind: 'subscription', planId: plan.id };
|
|
280
|
-
this.renderVariant();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** @param {number | string} quantity */
|
|
284
|
-
setQuantity(quantity) {
|
|
285
|
-
const minimum = has(this.root, 'quantity') ? 0 : 1;
|
|
286
|
-
this.quantity = Math.max(minimum, parseInt(String(quantity), 10) || 0);
|
|
287
|
-
this.$(sel('qty')).forEach((el) => {
|
|
288
|
-
setText(el, this.quantity);
|
|
289
|
-
});
|
|
290
|
-
this.renderVariant();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** @returns {Promise<unknown>} The cart after adding; `null` when the variant can't be added. */
|
|
294
|
-
async addToCart() {
|
|
295
|
-
if (!this.variant?.availableForSale) return null;
|
|
296
|
-
const line = this.getLine();
|
|
297
|
-
if (!line || line.quantity < 1) return null;
|
|
298
|
-
const cart = await Cart.add(line);
|
|
299
|
-
emit('product-added', { product: this.product, variant: this.variant, line }, this.root);
|
|
300
|
-
return cart;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Learns the markup's option names when they are the product's, then writes the product. */
|
|
304
|
-
renderProduct() {
|
|
305
|
-
const { product } = this;
|
|
306
|
-
if (!knownOptionNames.has(product.handle)) {
|
|
307
|
-
const authored = this.$(`${sel('option-list')},select${sel('option')}`).map(
|
|
308
|
-
(el) => get(el, 'option-list') || get(el, 'option'),
|
|
309
|
-
);
|
|
310
|
-
if (authored.some((name) => product.options.some((option) => option.name === name))) {
|
|
311
|
-
rememberOptionNames([product]);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
writeProduct(this);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Derives the view of the current choice, keeps its variant, selection and purchase, and writes it. */
|
|
318
|
-
renderVariant() {
|
|
319
|
-
const view = productView(
|
|
320
|
-
this.product,
|
|
321
|
-
{ selected: this.selected, purchase: this.purchaseChoice, quantity: this.quantity },
|
|
322
|
-
cfg.lowStock,
|
|
323
|
-
);
|
|
324
|
-
if (!view) {
|
|
325
|
-
this.variant = null;
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
this.variant = view.variant;
|
|
329
|
-
this.selected = view.selected;
|
|
330
|
-
this.purchaseChoice = view.purchase;
|
|
331
|
-
writeVariant(this, view);
|
|
332
|
-
emit(
|
|
333
|
-
'variant-changed',
|
|
334
|
-
{
|
|
335
|
-
product: this.product,
|
|
336
|
-
variant: view.variant,
|
|
337
|
-
selected: { ...view.selected },
|
|
338
|
-
purchase: view.purchase.kind,
|
|
339
|
-
sellingPlanId: view.isSubscription ? view.purchase.planId : null,
|
|
340
|
-
},
|
|
341
|
-
this.root,
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}
|
package/src/repeaters.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lists rendered from a template: a repeater's first child in the designer is the template, and every item gets a
|
|
3
|
-
* copy of it. The other children are placeholder content and are removed.
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, reportError, warn } from './config.js';
|
|
6
|
-
|
|
7
|
-
/** @type {WeakMap<Element, HTMLElement | null>} Each repeater's template, captured on first render. */
|
|
8
|
-
const templates = new WeakMap();
|
|
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));
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A deep copy of a template. `cloneNode` is typed as returning a `Node`; a copy of an element is an element of the
|
|
18
|
-
* same kind.
|
|
19
|
-
*
|
|
20
|
-
* @param {HTMLElement} template
|
|
21
|
-
*/
|
|
22
|
-
const copyOf = (template) => /** @type {HTMLElement} */ (template.cloneNode(true));
|
|
23
|
-
|
|
24
|
-
/** @param {Element} container */
|
|
25
|
-
function templateOf(container) {
|
|
26
|
-
const known = templates.get(container);
|
|
27
|
-
if (known !== undefined) return known;
|
|
28
|
-
const first = container.firstElementChild;
|
|
29
|
-
// Repeaters are HTML lists in the designer; their first child is an HTML element.
|
|
30
|
-
const template = first ? copyOf(/** @type {HTMLElement} */ (first)) : null;
|
|
31
|
-
templates.set(container, template);
|
|
32
|
-
container.replaceChildren();
|
|
33
|
-
return template;
|
|
34
|
-
}
|
|
35
|
-
|
|
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) {
|
|
45
|
-
const template = templateOf(container);
|
|
46
|
-
if (!template) {
|
|
47
|
-
if (items.length) warn('repeater without a template child', container);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
container.replaceChildren();
|
|
51
|
-
items.forEach((item, index) => {
|
|
52
|
-
const node = copyOf(template);
|
|
53
|
-
render(node, item, index);
|
|
54
|
-
container.appendChild(node);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
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
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {HTMLElement} node
|
|
67
|
-
* @param {KeyedHooks['onLeave']} onLeave
|
|
68
|
-
*/
|
|
69
|
-
function leave(node, onLeave) {
|
|
70
|
-
const done = () => {
|
|
71
|
-
node.remove();
|
|
72
|
-
};
|
|
73
|
-
if (!onLeave) {
|
|
74
|
-
done();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
onLeave(node, done);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
reportError(error, 'repeater onLeave');
|
|
81
|
-
done();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Inserts the nodes of new items next to the item before them, leaving the existing nodes where they are.
|
|
87
|
-
*
|
|
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
|
|
92
|
-
*/
|
|
93
|
-
function insertFresh(container, wanted, fresh, onEnter) {
|
|
94
|
-
wanted.forEach((node, index) => {
|
|
95
|
-
if (!fresh.has(node)) return;
|
|
96
|
-
const previous = wanted[index - 1];
|
|
97
|
-
if (previous?.parentNode === container) previous.after(node);
|
|
98
|
-
else container.prepend(node);
|
|
99
|
-
if (!onEnter) return;
|
|
100
|
-
try {
|
|
101
|
-
onEnter(node);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
reportError(error, 'repeater onEnter');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Keyed repeater: keeps existing nodes, inserts new ones, removes old ones. Used for cart lines, so values can
|
|
110
|
-
* 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
|
-
*/
|
|
119
|
-
export function repeatKeyed(container, items, keyOf, render, { onEnter, onLeave } = {}) {
|
|
120
|
-
const template = templateOf(container);
|
|
121
|
-
if (!template) {
|
|
122
|
-
warn('repeater without a template child', container);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const keyAttribute = `data-${cfg.prefix}-key`;
|
|
127
|
-
const leavingAttribute = `data-${cfg.prefix}-leaving`;
|
|
128
|
-
const live = () =>
|
|
129
|
-
[.../** @type {HTMLCollectionOf<HTMLElement>} */ (container.children)].filter(
|
|
130
|
-
(node) => node.hasAttribute(keyAttribute) && !node.hasAttribute(leavingAttribute),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
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();
|
|
138
|
-
|
|
139
|
-
items.forEach((item, index) => {
|
|
140
|
-
const key = String(keyOf(item, index));
|
|
141
|
-
let node = existing.get(key);
|
|
142
|
-
if (node) existing.delete(key);
|
|
143
|
-
else {
|
|
144
|
-
node = copyOf(template);
|
|
145
|
-
node.setAttribute(keyAttribute, key);
|
|
146
|
-
fresh.add(node);
|
|
147
|
-
}
|
|
148
|
-
render(node, item, index, fresh.has(node));
|
|
149
|
-
wanted.push(node);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const staying = wanted.filter((node) => !fresh.has(node));
|
|
153
|
-
const current = live().filter((node) => staying.includes(node));
|
|
154
|
-
const inOrder = current.length === staying.length && current.every((node, index) => node === staying[index]);
|
|
155
|
-
|
|
156
|
-
if (!inOrder) {
|
|
157
|
-
existing.forEach((node) => {
|
|
158
|
-
node.remove();
|
|
159
|
-
});
|
|
160
|
-
wanted.forEach((node) => container.appendChild(node));
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
insertFresh(container, wanted, fresh, onEnter);
|
|
165
|
-
|
|
166
|
-
existing.forEach((node) => {
|
|
167
|
-
node.setAttribute(leavingAttribute, '');
|
|
168
|
-
node.removeAttribute(keyAttribute);
|
|
169
|
-
node.style.pointerEvents = 'none';
|
|
170
|
-
node.inert = true;
|
|
171
|
-
leave(node, onLeave);
|
|
172
|
-
});
|
|
173
|
-
}
|