@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset for sites with page transitions (Barba, Swup), smooth scroll (Lenis) and GSAP. Nothing is read from globals
|
|
3
|
+
* except GSAP and ScrollTrigger by default: pass in what your site has.
|
|
4
|
+
*
|
|
5
|
+
* import Tether from "@occluze/tether";
|
|
6
|
+
* import { barbaPreset } from "@occluze/tether/presets/barba";
|
|
7
|
+
*
|
|
8
|
+
* Tether.init({
|
|
9
|
+
* ...window.TETHER_CONFIG,
|
|
10
|
+
* ...barbaPreset({
|
|
11
|
+
* lenis: () => myLenis, // resized after async renders
|
|
12
|
+
* lockScroll: (drawer) => myLock.lock(drawer), // when the cart opens
|
|
13
|
+
* unlockScroll: () => myLock.unlock(), // when the cart closes
|
|
14
|
+
* onValueChange: (el, previous) => {}, // e.g. a text scramble
|
|
15
|
+
* }),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // once per page container, e.g. in Barba's beforeEnter AND once():
|
|
19
|
+
* Tether.mount(container);
|
|
20
|
+
* // when leaving a page:
|
|
21
|
+
* Tether.unmount();
|
|
22
|
+
*/
|
|
23
|
+
import type { TetherConfig } from '../config.ts';
|
|
24
|
+
|
|
25
|
+
// GSAP isn't a dependency, so the two calls this preset makes are described here.
|
|
26
|
+
type TweenVars = Record<string, string | number | (() => void)>;
|
|
27
|
+
interface Gsap {
|
|
28
|
+
fromTo: (target: HTMLElement, from: TweenVars, to: TweenVars) => unknown;
|
|
29
|
+
to: (target: HTMLElement, to: TweenVars) => unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `gsap`, `lenis` and `scrollTrigger` are getters, read each time they are used, so an instance made later is found. */
|
|
33
|
+
interface BarbaPresetOptions {
|
|
34
|
+
/** Default `window.gsap`. */
|
|
35
|
+
gsap?: () => unknown;
|
|
36
|
+
/** Resized after async renders. */
|
|
37
|
+
lenis?: (() => unknown) | null;
|
|
38
|
+
/** Default `window.ScrollTrigger`, refreshed after async renders. */
|
|
39
|
+
scrollTrigger?: () => unknown;
|
|
40
|
+
/** When the cart opens. */
|
|
41
|
+
lockScroll?: ((drawer: HTMLElement) => void) | null;
|
|
42
|
+
/** When the cart closes. */
|
|
43
|
+
unlockScroll?: (() => void) | null;
|
|
44
|
+
/** After a cart line animated in or out. */
|
|
45
|
+
onResize?: (() => void) | null;
|
|
46
|
+
onValueChange?: TetherConfig['onValueChange'];
|
|
47
|
+
/** Animate cart lines in and out (default true). */
|
|
48
|
+
animateLines?: boolean;
|
|
49
|
+
/** Seconds. */
|
|
50
|
+
lineDuration?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The hooks the preset always sets, and `onValueChange` as passed (`null` when it isn't). */
|
|
54
|
+
type BarbaPresetConfig = { autoMount: false } & {
|
|
55
|
+
[Hook in 'onLayoutChange' | 'onCartOpen' | 'onCartClose' | 'onLineEnter' | 'onLineLeave']: NonNullable<
|
|
56
|
+
TetherConfig[Hook]
|
|
57
|
+
>;
|
|
58
|
+
} & Pick<TetherConfig, 'onValueChange'>;
|
|
59
|
+
|
|
60
|
+
export type { BarbaPresetOptions, BarbaPresetConfig };
|
|
61
|
+
|
|
62
|
+
const LINE_DURATION_SECONDS = 0.3;
|
|
63
|
+
/** @returns A new object each time: GSAP writes into the vars it is given. */
|
|
64
|
+
const collapsed = (): TweenVars => ({
|
|
65
|
+
height: 0,
|
|
66
|
+
paddingTop: 0,
|
|
67
|
+
paddingBottom: 0,
|
|
68
|
+
borderBottomWidth: 0,
|
|
69
|
+
opacity: 0,
|
|
70
|
+
overflow: 'hidden',
|
|
71
|
+
});
|
|
72
|
+
const ANIMATED_PROPS = 'height,paddingTop,paddingBottom,borderBottomWidth,opacity,overflow';
|
|
73
|
+
|
|
74
|
+
const reducedMotion = () =>
|
|
75
|
+
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
76
|
+
|
|
77
|
+
const GETTER_OPTIONS = ['gsap', 'lenis', 'scrollTrigger'] as const;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @throws {TypeError} When a getter option is a value, which JavaScript callers can pass: a value that is a function
|
|
81
|
+
* (ScrollTrigger is a class) can't be told from a getter.
|
|
82
|
+
*/
|
|
83
|
+
function checkGetters(options: BarbaPresetOptions) {
|
|
84
|
+
for (const name of GETTER_OPTIONS) {
|
|
85
|
+
const option: unknown = options[name];
|
|
86
|
+
if (option !== undefined && option !== null && typeof option !== 'function') {
|
|
87
|
+
throw new TypeError(`barbaPreset: ${name} must be a function that returns it, such as () => ${name}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calls `target[method]()` when it has one: Lenis and ScrollTrigger are optional and versions differ.
|
|
94
|
+
*
|
|
95
|
+
* @param target An object, or a class with a static method (`ScrollTrigger.refresh()`).
|
|
96
|
+
*/
|
|
97
|
+
function callIfPresent(target: unknown, method: string) {
|
|
98
|
+
if ((typeof target !== 'object' && typeof target !== 'function') || target === null) return;
|
|
99
|
+
const fn: unknown = Reflect.get(target, method);
|
|
100
|
+
// A method of `target`, called on it.
|
|
101
|
+
if (typeof fn === 'function') (fn as (this: unknown) => unknown).call(target);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isGsap = (value: unknown): value is Gsap =>
|
|
105
|
+
typeof value === 'object' &&
|
|
106
|
+
value !== null &&
|
|
107
|
+
typeof Reflect.get(value, 'fromTo') === 'function' &&
|
|
108
|
+
typeof Reflect.get(value, 'to') === 'function';
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Hooks for sites with page transitions, smooth scroll and GSAP: spread into `Tether.init()`.
|
|
112
|
+
*/
|
|
113
|
+
export function barbaPreset(options: BarbaPresetOptions = {}): BarbaPresetConfig {
|
|
114
|
+
checkGetters(options);
|
|
115
|
+
const {
|
|
116
|
+
gsap = (): unknown => Reflect.get(window, 'gsap'),
|
|
117
|
+
lenis = null,
|
|
118
|
+
scrollTrigger = (): unknown => Reflect.get(window, 'ScrollTrigger'),
|
|
119
|
+
lockScroll = null,
|
|
120
|
+
unlockScroll = null,
|
|
121
|
+
onResize = null,
|
|
122
|
+
onValueChange = null,
|
|
123
|
+
animateLines = true,
|
|
124
|
+
lineDuration = LINE_DURATION_SECONDS,
|
|
125
|
+
} = options;
|
|
126
|
+
|
|
127
|
+
/** Whether this preset holds the scroll lock, so a modal's own lock stays intact. */
|
|
128
|
+
let locked = false;
|
|
129
|
+
|
|
130
|
+
/** @returns GSAP, when this line should animate. */
|
|
131
|
+
const animator = (node: HTMLElement): Gsap | null => {
|
|
132
|
+
if (!animateLines || reducedMotion() || node.offsetHeight <= 0) return null;
|
|
133
|
+
const animation = gsap();
|
|
134
|
+
return isGsap(animation) ? animation : null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
autoMount: false,
|
|
139
|
+
|
|
140
|
+
onLayoutChange() {
|
|
141
|
+
callIfPresent(lenis?.(), 'resize');
|
|
142
|
+
callIfPresent(scrollTrigger(), 'refresh');
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onCartOpen(drawers) {
|
|
146
|
+
const drawer = drawers[0];
|
|
147
|
+
if (locked || !lockScroll || !drawer) return;
|
|
148
|
+
lockScroll(drawer);
|
|
149
|
+
locked = true;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
onCartClose() {
|
|
153
|
+
if (locked && unlockScroll) unlockScroll();
|
|
154
|
+
locked = false;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
onLineEnter(node) {
|
|
158
|
+
const animation = animator(node);
|
|
159
|
+
if (!animation) return;
|
|
160
|
+
const style = getComputedStyle(node);
|
|
161
|
+
animation.fromTo(node, collapsed(), {
|
|
162
|
+
height: node.offsetHeight,
|
|
163
|
+
paddingTop: style.paddingTop,
|
|
164
|
+
paddingBottom: style.paddingBottom,
|
|
165
|
+
borderBottomWidth: style.borderBottomWidth,
|
|
166
|
+
opacity: 1,
|
|
167
|
+
duration: lineDuration,
|
|
168
|
+
clearProps: ANIMATED_PROPS,
|
|
169
|
+
onComplete: () => onResize?.(),
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
onLineLeave(node, done) {
|
|
174
|
+
const animation = animator(node);
|
|
175
|
+
if (!animation) {
|
|
176
|
+
done();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
animation.to(node, {
|
|
180
|
+
...collapsed(),
|
|
181
|
+
duration: lineDuration,
|
|
182
|
+
onComplete: () => {
|
|
183
|
+
done();
|
|
184
|
+
onResize?.();
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
onValueChange,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @alias `barbaPreset`, for `import barbaPreset from '@occluze/tether/presets/barba'`. */
|
|
194
|
+
export default barbaPreset;
|
|
@@ -2,47 +2,33 @@
|
|
|
2
2
|
* A product scope's clicks and changes: option and plan buttons, purchase toggles, quantity steppers, the selects and
|
|
3
3
|
* add to cart. One delegated listener of each kind on the root, so cards a list re-renders need no new listeners.
|
|
4
4
|
*/
|
|
5
|
-
import { reportError } from './config.
|
|
6
|
-
import { sel, get, emit, setText, busy } from './dom.
|
|
7
|
-
import { addButtonLabel } from './variant-render.
|
|
8
|
-
|
|
5
|
+
import { reportError } from './config.ts';
|
|
6
|
+
import { sel, get, emit, setText, busy } from './dom.ts';
|
|
7
|
+
import { addButtonLabel } from './variant-render.ts';
|
|
8
|
+
import type { ProductScope } from './product.ts';
|
|
9
|
+
import type { Purchase } from './product-state.ts';
|
|
9
10
|
|
|
10
11
|
const productSelector = () => sel('product');
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* @returns {el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement}
|
|
15
|
-
*/
|
|
16
|
-
const isField = (el) => el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA';
|
|
13
|
+
const isField = (el: Element): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement =>
|
|
14
|
+
el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA';
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
* @param {EventTarget | null} target
|
|
20
|
-
* @returns {target is Element}
|
|
21
|
-
*/
|
|
22
|
-
const isElement = (target) => target instanceof window.Element;
|
|
16
|
+
const isElement = (target: EventTarget | null): target is Element => target instanceof window.Element;
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
|
-
* @
|
|
26
|
-
*
|
|
27
|
-
* @param {string} selector
|
|
28
|
-
* @returns {HTMLElement | null} The nearest `selector` around `target` that belongs to the scope, not to a nested one.
|
|
29
|
-
* Typed as an HTML element, as `qsa` types its matches.
|
|
19
|
+
* @returns The nearest `selector` around `target` that belongs to the scope, not to a nested one. Typed as an HTML
|
|
20
|
+
* element, as `qsa` types its matches.
|
|
30
21
|
*/
|
|
31
|
-
function closestOwn(scope, target, selector) {
|
|
32
|
-
const el =
|
|
22
|
+
function closestOwn(scope: ProductScope, target: Element, selector: string): HTMLElement | null {
|
|
23
|
+
const el = target.closest<HTMLElement>(selector);
|
|
33
24
|
return el?.closest(productSelector()) === scope.root ? el : null;
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* @param {ProductScope} scope
|
|
40
|
-
* @param {HTMLElement} button
|
|
41
|
-
*/
|
|
42
|
-
function onAddClick(scope, button) {
|
|
27
|
+
/** Adds to the cart with `aria-busy` on the button and `tether-adding-text` as its label meanwhile. */
|
|
28
|
+
function onAddClick(scope: ProductScope, button: HTMLElement): void {
|
|
43
29
|
const { label, original } = addButtonLabel(button);
|
|
44
30
|
const addingText = get(button, 'adding-text');
|
|
45
|
-
|
|
31
|
+
busy(button, async () => {
|
|
46
32
|
if (addingText) setText(label, addingText);
|
|
47
33
|
try {
|
|
48
34
|
await scope.addToCart();
|
|
@@ -52,18 +38,17 @@ function onAddClick(scope, button) {
|
|
|
52
38
|
} finally {
|
|
53
39
|
if (addingText) setText(label, original);
|
|
54
40
|
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
})
|
|
42
|
+
.then(() => {
|
|
43
|
+
// The mode-dependent labels (tether-subscribe-text, tether-soldout-text) wait for aria-busy to clear.
|
|
44
|
+
scope.renderVariant();
|
|
45
|
+
})
|
|
46
|
+
.catch((error: unknown) => {
|
|
47
|
+
reportError(error, 'add to cart render');
|
|
48
|
+
});
|
|
59
49
|
}
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
* @param {ProductScope} scope
|
|
63
|
-
* @param {Event} event
|
|
64
|
-
* @param {Element} target
|
|
65
|
-
*/
|
|
66
|
-
function onClick(scope, event, target) {
|
|
51
|
+
function onClick(scope: ProductScope, event: Event, target: Element): void {
|
|
67
52
|
const optionValue = closestOwn(scope, target, sel('option-value'));
|
|
68
53
|
const optionList = optionValue?.closest(sel('option-list'));
|
|
69
54
|
if (optionValue && optionList) {
|
|
@@ -81,7 +66,7 @@ function onClick(scope, event, target) {
|
|
|
81
66
|
if (purchase) {
|
|
82
67
|
// A radio or checkbox input keeps its own click, so it shows checked.
|
|
83
68
|
if (purchase.tagName !== 'INPUT') event.preventDefault();
|
|
84
|
-
scope.setPurchase(get(purchase, 'purchase'));
|
|
69
|
+
scope.setPurchase(purchaseKind(get(purchase, 'purchase')));
|
|
85
70
|
return;
|
|
86
71
|
}
|
|
87
72
|
const plus = closestOwn(scope, target, sel('qty-plus'));
|
|
@@ -98,11 +83,12 @@ function onClick(scope, event, target) {
|
|
|
98
83
|
}
|
|
99
84
|
}
|
|
100
85
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
/** @returns The kind a `tether-purchase` value asks for: `subscription`, else buying once. */
|
|
87
|
+
const purchaseKind = (value: string | null): Purchase['kind'] =>
|
|
88
|
+
value === 'subscription' ? 'subscription' : 'one-time';
|
|
89
|
+
|
|
90
|
+
/** @param target A form field in the scope. */
|
|
91
|
+
function onChange(scope: ProductScope, target: Element): void {
|
|
106
92
|
const value = isField(target) ? target.value : '';
|
|
107
93
|
if (target.matches(`select${sel('option')}`)) {
|
|
108
94
|
scope.select(scope.optName(get(target, 'option')), value);
|
|
@@ -112,14 +98,13 @@ function onChange(scope, target) {
|
|
|
112
98
|
if (value) scope.selectPlan(value);
|
|
113
99
|
else scope.setPurchase('one-time');
|
|
114
100
|
} else if (target.matches(`select${sel('purchase')}`)) {
|
|
115
|
-
scope.setPurchase(value);
|
|
101
|
+
scope.setPurchase(purchaseKind(value));
|
|
116
102
|
} else if (target.matches(sel('qty'))) {
|
|
117
103
|
scope.setQuantity(value);
|
|
118
104
|
}
|
|
119
105
|
}
|
|
120
106
|
|
|
121
|
-
|
|
122
|
-
export function bindProductEvents(scope) {
|
|
107
|
+
export function bindProductEvents(scope: ProductScope): void {
|
|
123
108
|
scope.root.addEventListener('click', (event) => {
|
|
124
109
|
if (isElement(event.target)) onClick(scope, event, event.target);
|
|
125
110
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The DOM writes of a product scope that depend on the product alone, once per product: its binds, the option and
|
|
3
|
-
* plan lists and the options of its selects. `variant-render.
|
|
3
|
+
* plan lists and the options of its selects. `variant-render.ts` writes what follows the visitor's choice.
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
6
|
attr,
|
|
@@ -18,23 +18,20 @@ import {
|
|
|
18
18
|
hasTemplate,
|
|
19
19
|
sameImage,
|
|
20
20
|
productUrl,
|
|
21
|
-
} from './dom.
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
} from './dom.ts';
|
|
22
|
+
import type { Product } from './fragments.ts';
|
|
23
|
+
import type { PlanInGroup } from './selling-plans.ts';
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* @typedef {{ color: string | null, image: { previewImage: Image | null } | null } | null} Swatch
|
|
37
|
-
*/
|
|
25
|
+
/** The product scope as its renders see it. */
|
|
26
|
+
export interface RenderScope {
|
|
27
|
+
readonly root: HTMLElement;
|
|
28
|
+
readonly product: Product;
|
|
29
|
+
readonly plans: PlanInGroup[];
|
|
30
|
+
/** Matches in this scope, the root included. */
|
|
31
|
+
$: (selector: string) => HTMLElement[];
|
|
32
|
+
/** The product's option name for one written in the markup. */
|
|
33
|
+
optName: (authored: string | null) => string;
|
|
34
|
+
}
|
|
38
35
|
|
|
39
36
|
/** `tether-bind="image:2"`: the second product image. */
|
|
40
37
|
const NTH_IMAGE_BIND = /^image:(\d+)$/;
|
|
@@ -43,26 +40,14 @@ const MULTI_LINE_METAFIELD = /^(rich_text|multi_line_text)_field$/;
|
|
|
43
40
|
/** `tether-plan-list` values that list every plan, not one group: Webflow's Designer can't always leave it empty. */
|
|
44
41
|
const ALL_PLAN_GROUPS = ['', 'true', 'all', '*'];
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
* @param {Element} el
|
|
48
|
-
* @returns {el is HTMLSelectElement}
|
|
49
|
-
*/
|
|
50
|
-
export const isSelect = (el) => el.tagName === 'SELECT';
|
|
43
|
+
export const isSelect = (el: Element): el is HTMLSelectElement => el.tagName === 'SELECT';
|
|
51
44
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* @returns {HTMLSelectElement[]} The `<select>` elements in the scope that match `selector`.
|
|
56
|
-
*/
|
|
57
|
-
export const selectsIn = (scope, selector) => scope.$(`select${selector}`).filter(isSelect);
|
|
45
|
+
/** @returns The `<select>` elements in the scope that match `selector`. */
|
|
46
|
+
export const selectsIn = (scope: RenderScope, selector: string): HTMLSelectElement[] =>
|
|
47
|
+
scope.$(`select${selector}`).filter(isSelect);
|
|
58
48
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
* @param {HTMLElement} el
|
|
63
|
-
* @param {Product} product
|
|
64
|
-
*/
|
|
65
|
-
function renderGallery(el, product) {
|
|
49
|
+
/** Leaves a server-rendered gallery with the same images as it is. */
|
|
50
|
+
function renderGallery(el: HTMLElement, product: Product): void {
|
|
66
51
|
const images = product.images.nodes;
|
|
67
52
|
const rendered = [...el.children].map((node) => (node.tagName === 'IMG' ? node : node.querySelector('img')));
|
|
68
53
|
const same =
|
|
@@ -76,12 +61,8 @@ function renderGallery(el, product) {
|
|
|
76
61
|
});
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
* @param {Product} product
|
|
82
|
-
* @param {string} ref `namespace.key`
|
|
83
|
-
*/
|
|
84
|
-
function renderMetafield(el, product, ref) {
|
|
64
|
+
/** @param ref `namespace.key` */
|
|
65
|
+
function renderMetafield(el: HTMLElement, product: Product, ref: string): void {
|
|
85
66
|
const dot = ref.indexOf('.');
|
|
86
67
|
const namespace = ref.slice(0, dot);
|
|
87
68
|
const key = ref.slice(dot + 1);
|
|
@@ -99,8 +80,8 @@ function renderMetafield(el, product, ref) {
|
|
|
99
80
|
setText(el, metafield.value);
|
|
100
81
|
}
|
|
101
82
|
|
|
102
|
-
/**
|
|
103
|
-
const PRODUCT_TEXTS = new Map(
|
|
83
|
+
/** The product binds written as text. */
|
|
84
|
+
const PRODUCT_TEXTS = new Map<string, (product: Product) => string>(
|
|
104
85
|
Object.entries({
|
|
105
86
|
title: (product) => product.title,
|
|
106
87
|
'description-text': (product) => product.description,
|
|
@@ -113,11 +94,7 @@ const PRODUCT_TEXTS = new Map(
|
|
|
113
94
|
}),
|
|
114
95
|
);
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
* @param {HTMLElement} el
|
|
118
|
-
* @param {Product} product
|
|
119
|
-
*/
|
|
120
|
-
function renderTags(el, product) {
|
|
97
|
+
function renderTags(el: HTMLElement, product: Product): void {
|
|
121
98
|
const exclude = (el.getAttribute('exclude') ?? '')
|
|
122
99
|
.split(',')
|
|
123
100
|
.map((tag) => tag.trim())
|
|
@@ -130,12 +107,8 @@ function renderTags(el, product) {
|
|
|
130
107
|
|
|
131
108
|
/**
|
|
132
109
|
* A `tether-bind` of the product itself; binds of the variant are left to `writeVariant`.
|
|
133
|
-
*
|
|
134
|
-
* @param {HTMLElement} el
|
|
135
|
-
* @param {string} key
|
|
136
|
-
* @param {Product} product
|
|
137
110
|
*/
|
|
138
|
-
function renderProductBind(el, key, product) {
|
|
111
|
+
function renderProductBind(el: HTMLElement, key: string, product: Product): void {
|
|
139
112
|
const text = PRODUCT_TEXTS.get(key);
|
|
140
113
|
if (text) {
|
|
141
114
|
setText(el, text(product));
|
|
@@ -149,10 +122,12 @@ function renderProductBind(el, key, product) {
|
|
|
149
122
|
setImage(el, product.featuredImage?.url, product.featuredImage?.altText || product.title);
|
|
150
123
|
break;
|
|
151
124
|
case 'url':
|
|
152
|
-
case 'link':
|
|
153
|
-
|
|
125
|
+
case 'link': {
|
|
126
|
+
const url = productUrl(product.handle, product.id);
|
|
127
|
+
if (url) el.setAttribute('href', url);
|
|
154
128
|
if (key === 'url' && !el.firstElementChild && !el.textContent.trim()) setText(el, product.title);
|
|
155
129
|
break;
|
|
130
|
+
}
|
|
156
131
|
case 'images':
|
|
157
132
|
renderGallery(el, product);
|
|
158
133
|
break;
|
|
@@ -164,14 +139,8 @@ function renderProductBind(el, key, product) {
|
|
|
164
139
|
}
|
|
165
140
|
}
|
|
166
141
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
*
|
|
170
|
-
* @param {HTMLElement} el
|
|
171
|
-
* @param {string} key
|
|
172
|
-
* @param {Product} product
|
|
173
|
-
*/
|
|
174
|
-
function renderIndexedBind(el, key, product) {
|
|
142
|
+
/** `image:<n>` (falling back to the featured image) and `metafield:<namespace.key>`. */
|
|
143
|
+
function renderIndexedBind(el: HTMLElement, key: string, product: Product): void {
|
|
175
144
|
const nth = NTH_IMAGE_BIND.exec(key);
|
|
176
145
|
if (nth) {
|
|
177
146
|
const image = product.images.nodes[Number(nth[1]) - 1] ?? product.featuredImage;
|
|
@@ -181,8 +150,7 @@ function renderIndexedBind(el, key, product) {
|
|
|
181
150
|
}
|
|
182
151
|
}
|
|
183
152
|
|
|
184
|
-
|
|
185
|
-
function renderOptionLists(scope) {
|
|
153
|
+
function renderOptionLists(scope: RenderScope): void {
|
|
186
154
|
const { product } = scope;
|
|
187
155
|
scope.$(sel('option-list')).forEach((container) => {
|
|
188
156
|
const name = scope.optName(get(container, 'option-list'));
|
|
@@ -197,8 +165,7 @@ function renderOptionLists(scope) {
|
|
|
197
165
|
if (text) setText(text, value.name);
|
|
198
166
|
else if (!node.firstElementChild) setText(node, value.name);
|
|
199
167
|
const swatchEl = qsa(node, sel('bind', 'swatch'))[0];
|
|
200
|
-
|
|
201
|
-
const swatch = /** @type {Swatch} */ (value.swatch);
|
|
168
|
+
const { swatch } = value;
|
|
202
169
|
if (!swatchEl || !swatch) return;
|
|
203
170
|
if (swatch.color) swatchEl.style.backgroundColor = swatch.color;
|
|
204
171
|
if (swatch.image?.previewImage) setImage(swatchEl, swatch.image.previewImage.url, value.name);
|
|
@@ -206,8 +173,7 @@ function renderOptionLists(scope) {
|
|
|
206
173
|
});
|
|
207
174
|
}
|
|
208
175
|
|
|
209
|
-
|
|
210
|
-
function renderSelects(scope) {
|
|
176
|
+
function renderSelects(scope: RenderScope): void {
|
|
211
177
|
const { product } = scope;
|
|
212
178
|
selectsIn(scope, sel('option')).forEach((select) => {
|
|
213
179
|
const option = product.options.find((candidate) => candidate.name === scope.optName(get(select, 'option')));
|
|
@@ -228,8 +194,7 @@ function renderSelects(scope) {
|
|
|
228
194
|
});
|
|
229
195
|
}
|
|
230
196
|
|
|
231
|
-
|
|
232
|
-
function renderPlanLists(scope) {
|
|
197
|
+
function renderPlanLists(scope: RenderScope): void {
|
|
233
198
|
scope.$(sel('plan-list')).forEach((container) => {
|
|
234
199
|
// The value is a selling plan group's name, or one of ALL_PLAN_GROUPS.
|
|
235
200
|
const raw = (get(container, 'plan-list') ?? '').trim();
|
|
@@ -242,12 +207,8 @@ function renderPlanLists(scope) {
|
|
|
242
207
|
});
|
|
243
208
|
}
|
|
244
209
|
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
*
|
|
248
|
-
* @param {RenderScope} scope
|
|
249
|
-
*/
|
|
250
|
-
export function writeProduct(scope) {
|
|
210
|
+
/** Writes what depends on the product alone: its binds, option and plan lists and the selects' options. */
|
|
211
|
+
export function writeProduct(scope: RenderScope): void {
|
|
251
212
|
scope.$(sel('bind')).forEach((el) => {
|
|
252
213
|
renderProductBind(el, get(el, 'bind') ?? '', scope.product);
|
|
253
214
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structure of Tether's product fragment, checked on a value read from the page or from a Storefront answer
|
|
3
|
+
* before it is used as a `Product`. Rendering walks the product's lists and prices without checking each step, so a
|
|
4
|
+
* truncated or hand-written entry, or an answer missing fields, would otherwise fail deep inside a render instead of
|
|
5
|
+
* being skipped or refused.
|
|
6
|
+
*/
|
|
7
|
+
import { matches } from './client.ts';
|
|
8
|
+
import { field, isRecord } from '../server/src/shared/json.ts';
|
|
9
|
+
import type { Shape } from './client.ts';
|
|
10
|
+
import type { Product } from './fragments.ts';
|
|
11
|
+
import type { PageInfo } from './loader.ts';
|
|
12
|
+
|
|
13
|
+
/** A page of a GraphQL connection of products. */
|
|
14
|
+
export interface ProductConnection {
|
|
15
|
+
nodes: Product[];
|
|
16
|
+
pageInfo: PageInfo;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A Storefront API `MoneyV2`. */
|
|
20
|
+
export const MONEY = { amount: 'string', currencyCode: 'string' } as const satisfies Shape;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A product's id, handle and title, and every list and price a render walks without a fallback: images, options,
|
|
24
|
+
* variants (each with an id, a price and its options) and the price range. Text fields are written as text, and the
|
|
25
|
+
* selling plan lists are read with a fallback, so they need no check.
|
|
26
|
+
*/
|
|
27
|
+
export const PRODUCT = {
|
|
28
|
+
id: 'string',
|
|
29
|
+
handle: 'string',
|
|
30
|
+
title: 'string',
|
|
31
|
+
options: ['object'],
|
|
32
|
+
images: { nodes: ['object'] },
|
|
33
|
+
priceRange: { minVariantPrice: MONEY, maxVariantPrice: MONEY },
|
|
34
|
+
variants: { nodes: [{ id: 'string', price: MONEY, selectedOptions: ['object'] }] },
|
|
35
|
+
} as const satisfies Shape;
|
|
36
|
+
|
|
37
|
+
/** A page of products, with the `pageInfo` paging reads. */
|
|
38
|
+
export const PRODUCT_CONNECTION = { nodes: [PRODUCT], pageInfo: { hasNextPage: 'boolean' } } as const satisfies Shape;
|
|
39
|
+
|
|
40
|
+
/** True for a value with the structure of `PRODUCT` and a non-empty id and handle. */
|
|
41
|
+
export const isProductShape = (value: unknown): value is Product =>
|
|
42
|
+
field(value, 'id') !== '' && field(value, 'handle') !== '' && matches(value, PRODUCT);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A node a lookup by gid answers: a product, or a node of another type, for which `... on Product` selects no field
|
|
46
|
+
* (no `id`).
|
|
47
|
+
*/
|
|
48
|
+
export const PRODUCT_NODE = (node: unknown): boolean =>
|
|
49
|
+
isRecord(node) && (node['id'] === undefined || matches(node, PRODUCT));
|