@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
|
@@ -3,26 +3,27 @@
|
|
|
3
3
|
* first change in another tab) and shareable variant links (on the first variant change, with `variantUrl` or
|
|
4
4
|
* `tether-variant-url`).
|
|
5
5
|
*/
|
|
6
|
-
import { cfg, warn, reportError } from './config.
|
|
7
|
-
import { sel, get, has, qsa, emit, parseJSONAttr,
|
|
8
|
-
import { scopes } from './product.
|
|
9
|
-
import { Cart } from './cart.
|
|
10
|
-
|
|
6
|
+
import { cfg, warn, reportError } from './config.ts';
|
|
7
|
+
import { sel, get, has, qsa, emit, parseJSONAttr, attributesFromJson } from './dom.ts';
|
|
8
|
+
import { scopes } from './product.ts';
|
|
9
|
+
import { Cart } from './cart.ts';
|
|
10
|
+
import { isRecord } from '../server/src/shared/json.ts';
|
|
11
|
+
import type { ShopifyCart } from './cart/store.ts';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
interface StoredMarket {
|
|
14
|
+
country: string;
|
|
15
|
+
language: string | null;
|
|
16
|
+
locale: string | undefined;
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
19
|
+
type SetCountry = (
|
|
20
|
+
country: string,
|
|
21
|
+
options: { language: string | null; locale: string | undefined; remember: boolean },
|
|
22
|
+
) => Promise<unknown>;
|
|
23
|
+
|
|
24
|
+
/** @param name The group's `tether-group` value. */
|
|
25
|
+
function groupAttributes(group: Element, name: string | null) {
|
|
26
|
+
const extra = attributesFromJson(parseJSONAttr(group, 'line-attributes'));
|
|
26
27
|
return name ? [{ key: '_group', value: name }, ...extra] : extra;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -30,10 +31,9 @@ function groupAttributes(group, name) {
|
|
|
30
31
|
* `[tether-group]` with `[tether-group-add]`: adds every product scope inside the group in one request, for bundles,
|
|
31
32
|
* build-a-box and "complete the set". The lines get the attribute `_group` and the group's `tether-line-attributes`.
|
|
32
33
|
*
|
|
33
|
-
* @
|
|
34
|
-
* @returns {Promise<ShopifyCart | null>} The cart, `null` with nothing to add or after a reported failure.
|
|
34
|
+
* @returns The cart, `null` with nothing to add or after a reported failure.
|
|
35
35
|
*/
|
|
36
|
-
export async function addGroup(button) {
|
|
36
|
+
export async function addGroup(button: Element): Promise<ShopifyCart | null> {
|
|
37
37
|
const group = button.closest(sel('group'));
|
|
38
38
|
if (!group) return null;
|
|
39
39
|
const name = get(group, 'group');
|
|
@@ -58,22 +58,16 @@ export async function addGroup(button) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* @param {string} text
|
|
65
|
-
* @returns {StoredMarket | null}
|
|
66
|
-
*/
|
|
67
|
-
function parseStoredMarket(text) {
|
|
68
|
-
/** @type {unknown} */
|
|
69
|
-
let value;
|
|
61
|
+
/** Another tab writes the market as JSON; a value that isn't (written by something else under the key) is skipped. */
|
|
62
|
+
function parseStoredMarket(text: string): StoredMarket | null {
|
|
63
|
+
let value: unknown;
|
|
70
64
|
try {
|
|
71
65
|
value = JSON.parse(text);
|
|
72
66
|
} catch {
|
|
73
67
|
return null;
|
|
74
68
|
}
|
|
75
|
-
if (
|
|
76
|
-
const { country, language, locale } =
|
|
69
|
+
if (!isRecord(value)) return null;
|
|
70
|
+
const { country, language, locale } = value;
|
|
77
71
|
if (typeof country !== 'string' || !country) return null;
|
|
78
72
|
return {
|
|
79
73
|
country,
|
|
@@ -82,41 +76,36 @@ function parseStoredMarket(text) {
|
|
|
82
76
|
};
|
|
83
77
|
}
|
|
84
78
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
* @param {StorageEvent} event
|
|
89
|
-
*/
|
|
90
|
-
export function onStorage(event) {
|
|
79
|
+
/** Another tab changed the cart (it is loaded again here) or the visitor's market (this tab follows it). */
|
|
80
|
+
export function onStorage(event: StorageEvent): void {
|
|
91
81
|
if (event.key === cfg.storageKey || event.key === `${cfg.storageKey}:seen`) {
|
|
92
|
-
Cart.init().catch((
|
|
82
|
+
Cart.init().catch((error: unknown) => {
|
|
93
83
|
reportError(error, 'cart from another tab');
|
|
94
84
|
});
|
|
95
85
|
return;
|
|
96
86
|
}
|
|
97
87
|
if (event.key !== `${cfg.prefix}:market:${cfg.domain}` || !event.newValue) return;
|
|
98
88
|
const market = parseStoredMarket(event.newValue);
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
// The core's `setCountry` takes the `null` language and `undefined` locale stored here, which the public type of
|
|
90
|
+
// `window.Tether` leaves out.
|
|
91
|
+
const tether = Reflect.get(window, 'Tether') as { setCountry: SetCountry } | undefined;
|
|
92
|
+
if (!market || !tether) return;
|
|
101
93
|
const options = { language: market.language, locale: market.locale, remember: false };
|
|
102
|
-
|
|
94
|
+
tether.setCountry(market.country, options).catch((error: unknown) => {
|
|
103
95
|
reportError(error, 'market from another tab');
|
|
104
96
|
});
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
/**
|
|
108
|
-
const urlKeys = new WeakMap();
|
|
99
|
+
/** Scope root → its last variant and plan, so the first render doesn't write. */
|
|
100
|
+
const urlKeys = new WeakMap<Element, string>();
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
const numericId = (gid) => gid.split('/').pop() ?? gid;
|
|
102
|
+
const numericId = (gid: string) => gid.split('/').pop() ?? gid;
|
|
112
103
|
|
|
113
104
|
/**
|
|
114
105
|
* Safari throws a SecurityError past 100 history changes in 10 seconds; the address then keeps the previous choice
|
|
115
106
|
* until the next one.
|
|
116
|
-
*
|
|
117
|
-
* @param {URL} url
|
|
118
107
|
*/
|
|
119
|
-
function replaceUrlIfAllowed(url) {
|
|
108
|
+
function replaceUrlIfAllowed(url: URL) {
|
|
120
109
|
try {
|
|
121
110
|
window.history.replaceState(window.history.state, '', url);
|
|
122
111
|
} catch {
|
|
@@ -127,10 +116,12 @@ function replaceUrlIfAllowed(url) {
|
|
|
127
116
|
/**
|
|
128
117
|
* `?variant=` (and `selling_plan=`) follow the visitor's choice.
|
|
129
118
|
*
|
|
130
|
-
* @param
|
|
131
|
-
* @param {{ variant: { id: string }, sellingPlanId: string | null | undefined }} choice
|
|
119
|
+
* @param root The product scope's root.
|
|
132
120
|
*/
|
|
133
|
-
export function writeVariantUrl(
|
|
121
|
+
export function writeVariantUrl(
|
|
122
|
+
root: Element,
|
|
123
|
+
{ variant, sellingPlanId }: { variant: { id: string }; sellingPlanId: string | null | undefined },
|
|
124
|
+
): void {
|
|
134
125
|
const key = `${variant.id}|${sellingPlanId ?? ''}`;
|
|
135
126
|
const before = urlKeys.get(root);
|
|
136
127
|
urlKeys.set(root, key);
|
package/src/fragments.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GraphQL fragments of what Tether renders. They are built at call time because the list sizes and metafields come
|
|
3
|
+
* from the config; the browser passes its validated `cfg`, the server helpers their own. Operations put these after
|
|
4
|
+
* the operation, where `withContext` expects them.
|
|
5
|
+
*/
|
|
6
|
+
import { cfg } from './config.ts';
|
|
7
|
+
import { toMetafieldIdentifier } from './config-sections.ts';
|
|
8
|
+
import type { TetherConfig, MetafieldIdentifier } from './config.ts';
|
|
9
|
+
import type { Money } from './shopify-values.ts';
|
|
10
|
+
import type { StoreAccess } from './client.ts';
|
|
11
|
+
|
|
12
|
+
export interface Image {
|
|
13
|
+
url: string;
|
|
14
|
+
altText: string | null;
|
|
15
|
+
width: number | null;
|
|
16
|
+
height: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** An option value's swatch: a colour, an image, or both, as the merchant set them in the admin. */
|
|
20
|
+
interface Swatch {
|
|
21
|
+
color: string | null;
|
|
22
|
+
image: { previewImage: Image | null } | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SellingPlanAdjustmentValue =
|
|
26
|
+
| { __typename: 'SellingPlanPercentagePriceAdjustment'; adjustmentPercentage: number }
|
|
27
|
+
| { __typename: 'SellingPlanFixedAmountPriceAdjustment'; adjustmentAmount: Money }
|
|
28
|
+
| { __typename: 'SellingPlanFixedPriceAdjustment'; price: Money };
|
|
29
|
+
|
|
30
|
+
export interface SellingPlan {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string | null;
|
|
34
|
+
recurringDeliveries: boolean;
|
|
35
|
+
options: { name: string; value: string }[];
|
|
36
|
+
priceAdjustments: { orderCount: number | null; adjustmentValue: SellingPlanAdjustmentValue }[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SellingPlanGroup {
|
|
40
|
+
name: string;
|
|
41
|
+
appName: string | null;
|
|
42
|
+
options: { name: string; values: string[] }[];
|
|
43
|
+
sellingPlans: { nodes: SellingPlan[] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A variant's price on one plan. */
|
|
47
|
+
export interface SellingPlanAllocation {
|
|
48
|
+
sellingPlan: { id: string };
|
|
49
|
+
/** What the customer pays at checkout (more for a prepaid plan). */
|
|
50
|
+
checkoutChargeAmount: Money;
|
|
51
|
+
remainingBalanceChargeAmount: Money;
|
|
52
|
+
priceAdjustments: { price: Money; compareAtPrice: Money; perDeliveryPrice: Money; unitPrice: Money | null }[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProductVariant {
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
availableForSale: boolean;
|
|
59
|
+
currentlyNotInStock: boolean;
|
|
60
|
+
/** Absent when the store's token can't read inventory. */
|
|
61
|
+
quantityAvailable?: number | null;
|
|
62
|
+
sku: string | null;
|
|
63
|
+
barcode: string | null;
|
|
64
|
+
selectedOptions: { name: string; value: string }[];
|
|
65
|
+
image: Image | null;
|
|
66
|
+
price: Money;
|
|
67
|
+
compareAtPrice: Money | null;
|
|
68
|
+
unitPrice: Money | null;
|
|
69
|
+
sellingPlanAllocations: { nodes: SellingPlanAllocation[] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Metafield {
|
|
73
|
+
namespace: string;
|
|
74
|
+
key: string;
|
|
75
|
+
type: string;
|
|
76
|
+
value: string;
|
|
77
|
+
/** A file reference's image. */
|
|
78
|
+
reference: { image?: Image } | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The fields of `fragment TetherProduct`. */
|
|
82
|
+
export interface Product {
|
|
83
|
+
id: string;
|
|
84
|
+
handle: string;
|
|
85
|
+
title: string;
|
|
86
|
+
description: string;
|
|
87
|
+
descriptionHtml: string;
|
|
88
|
+
productType: string;
|
|
89
|
+
vendor: string;
|
|
90
|
+
tags: string[];
|
|
91
|
+
availableForSale: boolean;
|
|
92
|
+
publishedAt: string | null;
|
|
93
|
+
onlineStoreUrl: string | null;
|
|
94
|
+
requiresSellingPlan: boolean;
|
|
95
|
+
seo: { title: string | null; description: string | null };
|
|
96
|
+
featuredImage: Image | null;
|
|
97
|
+
images: { nodes: Image[] };
|
|
98
|
+
options: { name: string; optionValues: { name: string; swatch: Swatch | null }[] }[];
|
|
99
|
+
priceRange: { minVariantPrice: Money; maxVariantPrice: Money };
|
|
100
|
+
compareAtPriceRange: { minVariantPrice: Money; maxVariantPrice: Money };
|
|
101
|
+
/** Present when metafields are configured; `null` for one that isn't set. */
|
|
102
|
+
metafields?: (Metafield | null)[];
|
|
103
|
+
sellingPlanGroups: { nodes: SellingPlanGroup[] };
|
|
104
|
+
variants: { nodes: ProductVariant[] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type ProductFragmentConfig = Pick<
|
|
108
|
+
TetherConfig,
|
|
109
|
+
'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst' | 'metafields'
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
const MONEY = 'amount currencyCode';
|
|
113
|
+
/** What a token reads unless the store turned out to refuse more (client.ts). */
|
|
114
|
+
const FULL_ACCESS: StoreAccess = { inventory: true };
|
|
115
|
+
|
|
116
|
+
/** The stock field, for a token that may read inventory. */
|
|
117
|
+
const inventoryField = (access: StoreAccess): string => (access.inventory ? 'quantityAvailable' : '');
|
|
118
|
+
const IMAGE = 'url altText width height';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The `metafields(identifiers:)` field. A fragment can't declare variables, so the identifiers are written into the
|
|
122
|
+
* text: only valid ones (`toMetafieldIdentifier`), each quoted as a string literal.
|
|
123
|
+
*/
|
|
124
|
+
function metafieldsField(metafields: ProductFragmentConfig['metafields']) {
|
|
125
|
+
const identifiers: MetafieldIdentifier[] = [];
|
|
126
|
+
for (const ref of metafields) {
|
|
127
|
+
const identifier = toMetafieldIdentifier(ref);
|
|
128
|
+
if (identifier) identifiers.push(identifier);
|
|
129
|
+
}
|
|
130
|
+
if (!identifiers.length) return '';
|
|
131
|
+
const list = identifiers
|
|
132
|
+
.map(({ namespace, key }) => `{namespace: ${JSON.stringify(namespace)}, key: ${JSON.stringify(key)}}`)
|
|
133
|
+
.join(', ');
|
|
134
|
+
return `metafields(identifiers: [${list}]) {
|
|
135
|
+
namespace key type value
|
|
136
|
+
reference { ... on MediaImage { image { ${IMAGE} } } }
|
|
137
|
+
}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const SELLING_PLAN_FIELDS = `
|
|
141
|
+
id name description recurringDeliveries
|
|
142
|
+
options { name value }
|
|
143
|
+
priceAdjustments {
|
|
144
|
+
orderCount
|
|
145
|
+
adjustmentValue {
|
|
146
|
+
__typename
|
|
147
|
+
... on SellingPlanPercentagePriceAdjustment { adjustmentPercentage }
|
|
148
|
+
... on SellingPlanFixedAmountPriceAdjustment { adjustmentAmount { ${MONEY} } }
|
|
149
|
+
... on SellingPlanFixedPriceAdjustment { price { ${MONEY} } }
|
|
150
|
+
}
|
|
151
|
+
}`;
|
|
152
|
+
|
|
153
|
+
const ALLOCATION_FIELDS = `
|
|
154
|
+
sellingPlan { id }
|
|
155
|
+
checkoutChargeAmount { ${MONEY} }
|
|
156
|
+
remainingBalanceChargeAmount { ${MONEY} }
|
|
157
|
+
priceAdjustments {
|
|
158
|
+
price { ${MONEY} }
|
|
159
|
+
compareAtPrice { ${MONEY} }
|
|
160
|
+
perDeliveryPrice { ${MONEY} }
|
|
161
|
+
unitPrice { ${MONEY} }
|
|
162
|
+
}`;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param o The list sizes and metafields.
|
|
166
|
+
* @param access What the token may read.
|
|
167
|
+
* @returns `fragment TetherProduct on Product`.
|
|
168
|
+
*/
|
|
169
|
+
export function productFragment(o: ProductFragmentConfig = cfg, access: StoreAccess = FULL_ACCESS): string {
|
|
170
|
+
return `
|
|
171
|
+
fragment TetherProduct on Product {
|
|
172
|
+
id handle title description descriptionHtml productType vendor tags
|
|
173
|
+
availableForSale publishedAt onlineStoreUrl requiresSellingPlan
|
|
174
|
+
seo { title description }
|
|
175
|
+
featuredImage { ${IMAGE} }
|
|
176
|
+
images(first: ${o.imagesFirst}) { nodes { ${IMAGE} } }
|
|
177
|
+
options { name optionValues { name swatch { color image { previewImage { ${IMAGE} } } } } }
|
|
178
|
+
priceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
|
|
179
|
+
compareAtPriceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
|
|
180
|
+
${metafieldsField(o.metafields)}
|
|
181
|
+
sellingPlanGroups(first: ${o.sellingPlanGroupsFirst}) {
|
|
182
|
+
nodes {
|
|
183
|
+
name appName
|
|
184
|
+
options { name values }
|
|
185
|
+
sellingPlans(first: ${o.sellingPlansFirst}) { nodes { ${SELLING_PLAN_FIELDS} } }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
variants(first: ${o.variantsFirst}) {
|
|
189
|
+
nodes {
|
|
190
|
+
id title availableForSale currentlyNotInStock ${inventoryField(access)} sku barcode
|
|
191
|
+
selectedOptions { name value }
|
|
192
|
+
image { ${IMAGE} }
|
|
193
|
+
price { ${MONEY} }
|
|
194
|
+
compareAtPrice { ${MONEY} }
|
|
195
|
+
unitPrice { ${MONEY} }
|
|
196
|
+
sellingPlanAllocations(first: ${o.sellingPlansFirst}) { nodes { ${ALLOCATION_FIELDS} } }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param access What the token may read.
|
|
204
|
+
* @returns `fragment TetherCart on Cart`.
|
|
205
|
+
*/
|
|
206
|
+
export function cartFragment(access: StoreAccess = FULL_ACCESS): string {
|
|
207
|
+
return `
|
|
208
|
+
fragment TetherCart on Cart {
|
|
209
|
+
id checkoutUrl totalQuantity note createdAt updatedAt
|
|
210
|
+
attributes { key value }
|
|
211
|
+
buyerIdentity { countryCode email }
|
|
212
|
+
discountCodes { code applicable }
|
|
213
|
+
cost {
|
|
214
|
+
subtotalAmount { ${MONEY} }
|
|
215
|
+
totalAmount { ${MONEY} }
|
|
216
|
+
checkoutChargeAmount { ${MONEY} }
|
|
217
|
+
}
|
|
218
|
+
lines(first: 100) {
|
|
219
|
+
nodes {
|
|
220
|
+
id quantity
|
|
221
|
+
attributes { key value }
|
|
222
|
+
cost {
|
|
223
|
+
totalAmount { ${MONEY} }
|
|
224
|
+
subtotalAmount { ${MONEY} }
|
|
225
|
+
amountPerQuantity { ${MONEY} }
|
|
226
|
+
compareAtAmountPerQuantity { ${MONEY} }
|
|
227
|
+
}
|
|
228
|
+
discountAllocations { discountedAmount { ${MONEY} } }
|
|
229
|
+
sellingPlanAllocation {
|
|
230
|
+
sellingPlan { id name description recurringDeliveries options { name value } }
|
|
231
|
+
checkoutChargeAmount { ${MONEY} }
|
|
232
|
+
priceAdjustments { price { ${MONEY} } compareAtPrice { ${MONEY} } perDeliveryPrice { ${MONEY} } }
|
|
233
|
+
}
|
|
234
|
+
merchandise {
|
|
235
|
+
... on ProductVariant {
|
|
236
|
+
id title sku availableForSale requiresShipping ${inventoryField(access)}
|
|
237
|
+
image { ${IMAGE} }
|
|
238
|
+
price { ${MONEY} }
|
|
239
|
+
compareAtPrice { ${MONEY} }
|
|
240
|
+
selectedOptions { name value }
|
|
241
|
+
product { id title handle vendor featuredImage { ${IMAGE} } }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
... on ComponentizableCartLine {
|
|
245
|
+
lineComponents {
|
|
246
|
+
id quantity
|
|
247
|
+
merchandise { ... on ProductVariant { id title product { title handle } } }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The collection fields Tether renders, for inside a `collection { }` selection. */
|
|
256
|
+
export function collectionFields(): string {
|
|
257
|
+
return `
|
|
258
|
+
id handle title description descriptionHtml
|
|
259
|
+
image { ${IMAGE} }
|
|
260
|
+
seo { title description }`;
|
|
261
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Tether reads from and sets on `window`, for sites that use the `<script>` build or `window.Tether`. Only the
|
|
3
|
+
* published declarations carry it (scripts/build.ts adds it to the generated `index.d.ts`): the source reads these
|
|
4
|
+
* names as unknown and validates them, so they stay out of its own program.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tether, TetherConfig } from './index.ts';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
Tether: Tether;
|
|
11
|
+
TETHER_CONFIG?: TetherConfig;
|
|
12
|
+
TETHER_COUNTRY?: string;
|
|
13
|
+
TETHER_LANGUAGE?: string;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -2,25 +2,26 @@
|
|
|
2
2
|
* Shopify images on the page: sized through the CDN's URL parameters, written to an `<img>` or as a CSS background,
|
|
3
3
|
* and never downloaded twice when the server already rendered the same image.
|
|
4
4
|
*/
|
|
5
|
-
import { attr } from './markup.
|
|
5
|
+
import { attr } from './markup.ts';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/** Shopify CDN parameters. */
|
|
8
|
+
export interface ImageSize {
|
|
9
|
+
width?: string | number | null;
|
|
10
|
+
height?: string | number | null;
|
|
11
|
+
/** `center`, `top`, `bottom`, `left` or `right`; only applies with a width or height. */
|
|
12
|
+
crop?: string | null;
|
|
13
|
+
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Shopify CDN image at a size: `imageUrl(url, { width: 800 })`. Use the same width as the element's max-width
|
|
16
17
|
* attribute, so the browser script keeps the image instead of loading it again.
|
|
17
18
|
*
|
|
18
|
-
* @
|
|
19
|
-
* @param {Url} url
|
|
20
|
-
* @param {ImageSize} [size]
|
|
21
|
-
* @returns {Url} The sized URL; `url` itself when it is empty or not absolute.
|
|
19
|
+
* @returns The sized URL; `url` itself when it is empty or not absolute.
|
|
22
20
|
*/
|
|
23
|
-
export function imageUrl
|
|
21
|
+
export function imageUrl<Url extends string | null | undefined>(
|
|
22
|
+
url: Url,
|
|
23
|
+
{ width, height, crop }: ImageSize = {},
|
|
24
|
+
): Url {
|
|
24
25
|
if (!url) return url;
|
|
25
26
|
let parsed;
|
|
26
27
|
try {
|
|
@@ -33,59 +34,45 @@ export function imageUrl(url, { width, height, crop } = {}) {
|
|
|
33
34
|
if (width) parsed.searchParams.set('width', String(width));
|
|
34
35
|
if (height) parsed.searchParams.set('height', String(height));
|
|
35
36
|
if (crop) parsed.searchParams.set('crop', crop);
|
|
36
|
-
|
|
37
|
+
// `url` is a non-empty string here, so `Url` is a string type.
|
|
38
|
+
return parsed.toString() as Url;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
* @param {string} name
|
|
42
|
-
* @returns {string | null} `name` as Webflow's custom attribute, or in valid HTML as `<prefix>-name` / `data-name`.
|
|
43
|
-
*/
|
|
44
|
-
function sizeAttribute(el, name) {
|
|
41
|
+
/** @returns `name` as Webflow's custom attribute, or in valid HTML as `<prefix>-name` / `data-name`. */
|
|
42
|
+
function sizeAttribute(el: Element, name: string): string | null {
|
|
45
43
|
return el.getAttribute(name) ?? el.getAttribute(attr(name)) ?? el.getAttribute(`data-${name}`);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
* @param {Url} url
|
|
51
|
-
* @param {Element | null | undefined} el The element whose max-width / max-height / crop attributes size the image.
|
|
52
|
-
* @returns {Url}
|
|
53
|
-
*/
|
|
54
|
-
export function cdnUrl(url, el) {
|
|
46
|
+
/** @param el The element whose max-width / max-height / crop attributes size the image. */
|
|
47
|
+
export function cdnUrl<Url extends string | null | undefined>(url: Url, el: Element | null | undefined): Url {
|
|
55
48
|
if (!url || !el) return url;
|
|
56
49
|
const width = sizeAttribute(el, 'max-width');
|
|
57
50
|
const height = sizeAttribute(el, 'max-height');
|
|
58
51
|
return imageUrl(url, { width, height, crop: width || height ? sizeAttribute(el, 'crop') : null });
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
/** @returns The URL without its query (the size parameters). */
|
|
55
|
+
const withoutQuery = (url: string) => url.replace(/\?.*$/, '');
|
|
56
|
+
|
|
57
|
+
/** @returns True for the same image at any size parameters: the server-rendered one is kept. */
|
|
58
|
+
export function sameImage(a: string | null | undefined, b: string | null | undefined): boolean {
|
|
59
|
+
if (!a || !b) return false;
|
|
60
|
+
return withoutQuery(a) === withoutQuery(b);
|
|
61
|
+
}
|
|
67
62
|
|
|
68
63
|
/**
|
|
69
64
|
* A CSS `url()` for an image URL from Shopify. Characters that would end the string are escaped, so the URL can't
|
|
70
65
|
* add declarations; only http(s), relative and data:image URLs are used.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} url
|
|
73
66
|
*/
|
|
74
|
-
function cssUrl(url) {
|
|
67
|
+
function cssUrl(url: string): string {
|
|
75
68
|
const hasScheme = /^\s*[a-z][a-z0-9+.-]*:/i.test(url);
|
|
76
69
|
if (hasScheme && !/^\s*(https?:|data:image\/)/i.test(url)) return 'none';
|
|
77
70
|
const escaped = url.replace(/["\\\n\r\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
|
|
78
71
|
return `url("${escaped}")`;
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
* @param {HTMLElement} el
|
|
85
|
-
* @param {string | null | undefined} url
|
|
86
|
-
* @param {string | null} [alt]
|
|
87
|
-
*/
|
|
88
|
-
export function setImage(el, url, alt) {
|
|
74
|
+
/** Shows `url` in `el`: as the `src` of an `<img>` (lazy unless it says otherwise), else as its background image. */
|
|
75
|
+
export function setImage(el: HTMLElement, url: string | null | undefined, alt?: string | null): void {
|
|
89
76
|
if (!url) return;
|
|
90
77
|
const src = cdnUrl(url, el);
|
|
91
78
|
if (el.tagName !== 'IMG') {
|