@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/index.js
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tether's entry: `window.Tether`, `init()`, `mount()` and the start of the plain `<script>` setup.
|
|
3
|
-
*
|
|
4
|
-
* <script>window.TETHER_CONFIG = { domain: 'x.myshopify.com', token: '...' }</script>
|
|
5
|
-
* <script src="tether.min.js" defer></script>
|
|
6
|
-
*
|
|
7
|
-
* With page transitions (Barba, Swup): set `autoMount: false`, call `Tether.init()` once and
|
|
8
|
-
* `Tether.mount(container)` after every page enter.
|
|
9
|
-
*/
|
|
10
|
-
import { cfg, configure, isConfigured, log, warn, reportError } from './config.js';
|
|
11
|
-
import { gql, TetherError, withContext } from './client.js';
|
|
12
|
-
import { Cart } from './cart.js';
|
|
13
|
-
import { scopes } from './product.js';
|
|
14
|
-
import { ListScope, listSelector } from './collection.js';
|
|
15
|
-
import {
|
|
16
|
-
getProduct,
|
|
17
|
-
getProducts,
|
|
18
|
-
getRecommendations,
|
|
19
|
-
clearProductCache,
|
|
20
|
-
primeProduct,
|
|
21
|
-
normalizeProductKey,
|
|
22
|
-
} from './loader.js';
|
|
23
|
-
import * as plans from './selling-plans.js';
|
|
24
|
-
import { resolveProductKey } from './status.js';
|
|
25
|
-
import { sel, emit, formatMoney, imageUrl } from './dom.js';
|
|
26
|
-
import { loadPart, coreModules } from './lazy.js';
|
|
27
|
-
import {
|
|
28
|
-
mountedLists,
|
|
29
|
-
liveLists,
|
|
30
|
-
pruneLive,
|
|
31
|
-
within,
|
|
32
|
-
mountProduct,
|
|
33
|
-
hydrateFromPage,
|
|
34
|
-
settleMarket,
|
|
35
|
-
trackMarket,
|
|
36
|
-
} from './mounting.js';
|
|
37
|
-
import { applyStartMarket, setCountry } from './page-market.js';
|
|
38
|
-
import { accountApi, mountAccountIfUsed } from './account-api.js';
|
|
39
|
-
import { bindCartControls } from './controls/cart-controls.js';
|
|
40
|
-
import { bindDiscountControls } from './controls/discount.js';
|
|
41
|
-
import { addGroup, bindExtrasControls } from './controls/extras.js';
|
|
42
|
-
import { bindNavigation, rememberDocumentMount } from './controls/navigation.js';
|
|
43
|
-
/** @import { TetherConfig, TetherOptions, MarketsConfig } from './config.js' */
|
|
44
|
-
/** @import { ShopifyCart } from './cart/store.js' */
|
|
45
|
-
/** @import { ProductScope } from './product.js' */
|
|
46
|
-
/** @import { DiagnoseOptions } from './diagnose.js' */
|
|
47
|
-
|
|
48
|
-
export { addGroup, setCountry };
|
|
49
|
-
|
|
50
|
-
export const VERSION = '0.6.8';
|
|
51
|
-
|
|
52
|
-
/** The events the analytics part reports, buffered from `init()` until the part has loaded. */
|
|
53
|
-
const ANALYTICS_EVENTS = ['product-ready', 'list-ready', 'cart-updated', 'cart-toggled', 'checkout', 'search'];
|
|
54
|
-
|
|
55
|
-
/** Milliseconds `observe` waits after the last added node, so a burst of additions mounts once. */
|
|
56
|
-
const OBSERVE_DEBOUNCE_MS = 50;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @typedef {object} TetherGlobals What the page sets on `window` for Tether, and `window.Tether` itself.
|
|
60
|
-
* @property {TetherOptions} [TETHER_CONFIG] The site's config; `configure()` validates it.
|
|
61
|
-
* @property {string} [TETHER_INTEGRATION] Set by a framework integration (Astro) that starts Tether itself.
|
|
62
|
-
* @property {TetherApi} [Tether]
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/** @returns {Window & TetherGlobals} */
|
|
66
|
-
const pageGlobals = () => window;
|
|
67
|
-
|
|
68
|
-
/** @type {Promise<TetherApi | null> | null} */
|
|
69
|
-
let initPromise = null;
|
|
70
|
-
|
|
71
|
-
/** @type {MutationObserver | null} */
|
|
72
|
-
let observer = null;
|
|
73
|
-
|
|
74
|
-
/** @type {Event[] | null} The analytics part's queue, created once by the first `init()` with `analytics`. */
|
|
75
|
-
let analyticsQueue = null;
|
|
76
|
-
|
|
77
|
-
/** @type {boolean} The document-wide listeners are bound once, by the first configured `init()`. */
|
|
78
|
-
let controlsBound = false;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Mounts the lists of `root` and the product scopes in them as they render.
|
|
82
|
-
*
|
|
83
|
-
* @param {ParentNode} root
|
|
84
|
-
* @returns {number} The lists mounted.
|
|
85
|
-
*/
|
|
86
|
-
function mountLists(root) {
|
|
87
|
-
let count = 0;
|
|
88
|
-
within(root, listSelector()).forEach((el) => {
|
|
89
|
-
if (mountedLists.has(el)) return;
|
|
90
|
-
mountedLists.add(el);
|
|
91
|
-
count++;
|
|
92
|
-
const list = new ListScope(el);
|
|
93
|
-
liveLists.add(list);
|
|
94
|
-
el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
|
|
95
|
-
// The list emits this CustomEvent once its products are in the page.
|
|
96
|
-
mount(/** @type {CustomEvent<{ container: ParentNode }>} */ (event).detail.container);
|
|
97
|
-
});
|
|
98
|
-
void trackMarket(list.load());
|
|
99
|
-
});
|
|
100
|
-
return count;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Loads each optional part `root` uses: the country select, customer accounts and search as you type.
|
|
105
|
-
*
|
|
106
|
-
* @param {ParentNode} root
|
|
107
|
-
*/
|
|
108
|
-
function mountParts(root) {
|
|
109
|
-
if (within(root, `${sel('country-select')},${sel('country-bind')}`).length) {
|
|
110
|
-
loadPart('markets')
|
|
111
|
-
.then((part) => part.renderCountrySelects(root))
|
|
112
|
-
.catch((/** @type {unknown} */ error) => {
|
|
113
|
-
reportError(error, 'markets');
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
mountAccountIfUsed(root);
|
|
117
|
-
if (within(root, sel('search')).length) {
|
|
118
|
-
loadPart('search')
|
|
119
|
-
.then((part) => {
|
|
120
|
-
part.mountSearch(root);
|
|
121
|
-
})
|
|
122
|
-
.catch((/** @type {unknown} */ error) => {
|
|
123
|
-
reportError(error, 'search');
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Mounts every product scope and product list inside `root`. Safe to call repeatedly.
|
|
130
|
-
*
|
|
131
|
-
* @param {ParentNode} [root]
|
|
132
|
-
* @returns {number} The scopes and lists newly mounted.
|
|
133
|
-
*/
|
|
134
|
-
export function mount(root = document) {
|
|
135
|
-
if (!isConfigured()) return 0;
|
|
136
|
-
if (root === document) rememberDocumentMount();
|
|
137
|
-
// Page transitions detach old roots: they aren't kept alive.
|
|
138
|
-
pruneLive();
|
|
139
|
-
// Products the server rendered into the page need no request.
|
|
140
|
-
hydrateFromPage(root.ownerDocument ?? document);
|
|
141
|
-
let count = mountLists(root);
|
|
142
|
-
within(root, sel('product')).forEach((el) => {
|
|
143
|
-
if (mountProduct(el)) count++;
|
|
144
|
-
});
|
|
145
|
-
mountParts(root);
|
|
146
|
-
settleMarket();
|
|
147
|
-
Cart.render();
|
|
148
|
-
log(`mounted ${count} scope(s)`, root);
|
|
149
|
-
return count;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Call before a page transition removes the page: closes the cart drawer. */
|
|
153
|
-
export function unmount() {
|
|
154
|
-
Cart.close();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* @param {Element | null | undefined} el
|
|
159
|
-
* @returns {ProductScope | null} The product scope `el` is in.
|
|
160
|
-
*/
|
|
161
|
-
export function getScope(el) {
|
|
162
|
-
const root = el?.closest(sel('product'));
|
|
163
|
-
return root ? (scopes.get(root) ?? null) : null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function bindControls() {
|
|
167
|
-
if (controlsBound) return;
|
|
168
|
-
controlsBound = true;
|
|
169
|
-
bindCartControls();
|
|
170
|
-
bindDiscountControls();
|
|
171
|
-
bindExtrasControls();
|
|
172
|
-
bindNavigation({ ready: () => initPromise ?? Promise.resolve(null), mount, unmount });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Buffers Tether's events until the analytics part has loaded, so none are missed. */
|
|
176
|
-
function startAnalyticsQueue() {
|
|
177
|
-
/** @type {Event[]} */
|
|
178
|
-
const queue = [];
|
|
179
|
-
analyticsQueue = queue;
|
|
180
|
-
ANALYTICS_EVENTS.forEach((name) => {
|
|
181
|
-
document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
|
|
182
|
-
});
|
|
183
|
-
document.addEventListener('astro:page-load', () => queue.push(new CustomEvent(`${cfg.prefix}:page`)));
|
|
184
|
-
loadPart('analytics')
|
|
185
|
-
.then((part) => part.startAnalytics(queue))
|
|
186
|
-
.catch((/** @type {unknown} */ error) => {
|
|
187
|
-
reportError(error, 'analytics');
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* A saved cart from another market moves to this one, so its prices match the page's.
|
|
193
|
-
*
|
|
194
|
-
* @param {ShopifyCart | null} cart
|
|
195
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
196
|
-
*/
|
|
197
|
-
async function cartInPageMarket(cart) {
|
|
198
|
-
const country = cfg.country?.toUpperCase();
|
|
199
|
-
const cartCountry = cart?.buyerIdentity.countryCode;
|
|
200
|
-
if (!cart || !country || !cartCountry || cartCountry === country) return cart;
|
|
201
|
-
try {
|
|
202
|
-
return await Cart.setBuyerIdentity({ countryCode: country }, { create: false });
|
|
203
|
-
} catch (error) {
|
|
204
|
-
reportError(error, 'cart country');
|
|
205
|
-
return cart;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Configures Tether (`window.TETHER_CONFIG` with `overrides` over it), applies the visitor's market and loads the
|
|
211
|
-
* cart. A second call without overrides returns the first one's promise.
|
|
212
|
-
*
|
|
213
|
-
* @param {TetherOptions} [overrides]
|
|
214
|
-
* @returns {Promise<TetherApi | null>} `null` without a domain and token, when Tether stays idle.
|
|
215
|
-
*/
|
|
216
|
-
export function init(overrides = {}) {
|
|
217
|
-
if (initPromise && !Object.keys(overrides).length) return initPromise;
|
|
218
|
-
configure({ ...pageGlobals().TETHER_CONFIG, ...overrides });
|
|
219
|
-
if (!isConfigured()) {
|
|
220
|
-
warn('TETHER_CONFIG is missing domain or token. Tether is idle.');
|
|
221
|
-
initPromise = Promise.resolve(null);
|
|
222
|
-
return initPromise;
|
|
223
|
-
}
|
|
224
|
-
bindControls();
|
|
225
|
-
if (cfg.observe) startObserver();
|
|
226
|
-
if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
|
|
227
|
-
initPromise = applyStartMarket(overrides)
|
|
228
|
-
.then(() => Cart.init())
|
|
229
|
-
.then(cartInPageMarket)
|
|
230
|
-
.then((cart) => {
|
|
231
|
-
emit('ready', { cart });
|
|
232
|
-
return api;
|
|
233
|
-
});
|
|
234
|
-
return initPromise;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** `observe`: mounts the elements added to the page later. */
|
|
238
|
-
function startObserver() {
|
|
239
|
-
if (observer || typeof MutationObserver === 'undefined') return;
|
|
240
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
241
|
-
let timer;
|
|
242
|
-
/** @type {Set<Element>} */
|
|
243
|
-
const queued = new Set();
|
|
244
|
-
observer = new MutationObserver((mutations) => {
|
|
245
|
-
mutations.forEach((mutation) => {
|
|
246
|
-
mutation.addedNodes.forEach((node) => {
|
|
247
|
-
if (node instanceof window.Element) queued.add(node);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
clearTimeout(timer);
|
|
251
|
-
timer = setTimeout(() => {
|
|
252
|
-
queued.forEach((node) => {
|
|
253
|
-
if (node.isConnected) mount(node);
|
|
254
|
-
});
|
|
255
|
-
queued.clear();
|
|
256
|
-
}, OBSERVE_DEBOUNCE_MS);
|
|
257
|
-
});
|
|
258
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* @param {MarketsConfig | null} [markets] Default `markets` of the config.
|
|
263
|
-
*/
|
|
264
|
-
const detectMarket = (markets) => loadPart('markets').then((part) => part.detectMarket(markets ?? cfg.markets));
|
|
265
|
-
|
|
266
|
-
const getLocalization = () => loadPart('markets').then((part) => part.getLocalization());
|
|
267
|
-
|
|
268
|
-
/** @param {Omit<DiagnoseOptions, 'version'>} [options] */
|
|
269
|
-
const diagnose = (options) => loadPart('diagnose').then((part) => part.diagnose({ ...options, version: VERSION }));
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Listens to the Tether event `<prefix>:<name>`.
|
|
273
|
-
*
|
|
274
|
-
* @param {string} name
|
|
275
|
-
* @param {(detail: unknown, event: CustomEvent) => void} listener
|
|
276
|
-
* @returns {() => void} Stops listening.
|
|
277
|
-
*/
|
|
278
|
-
function on(name, listener) {
|
|
279
|
-
/** @param {Event} event */
|
|
280
|
-
const handler = (event) => {
|
|
281
|
-
const custom = /** @type {CustomEvent} Tether's events are CustomEvents. */ (event);
|
|
282
|
-
listener(custom.detail, custom);
|
|
283
|
-
};
|
|
284
|
-
document.addEventListener(`${cfg.prefix}:${name}`, handler);
|
|
285
|
-
return () => {
|
|
286
|
-
document.removeEventListener(`${cfg.prefix}:${name}`, handler);
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export const api = {
|
|
291
|
-
VERSION,
|
|
292
|
-
init,
|
|
293
|
-
mount,
|
|
294
|
-
unmount,
|
|
295
|
-
/** @returns {TetherConfig} */
|
|
296
|
-
get config() {
|
|
297
|
-
return cfg;
|
|
298
|
-
},
|
|
299
|
-
/** @returns {Promise<TetherApi | null>} */
|
|
300
|
-
get ready() {
|
|
301
|
-
return initPromise ?? Promise.resolve(null);
|
|
302
|
-
},
|
|
303
|
-
query: gql,
|
|
304
|
-
withContext,
|
|
305
|
-
cart: Cart,
|
|
306
|
-
getProduct,
|
|
307
|
-
getProducts,
|
|
308
|
-
getRecommendations,
|
|
309
|
-
getScope,
|
|
310
|
-
addGroup,
|
|
311
|
-
account: accountApi,
|
|
312
|
-
primeProduct,
|
|
313
|
-
normalizeProductKey,
|
|
314
|
-
clearProductCache,
|
|
315
|
-
formatMoney,
|
|
316
|
-
imageUrl,
|
|
317
|
-
plans,
|
|
318
|
-
TetherError,
|
|
319
|
-
resolveProductKey,
|
|
320
|
-
setCountry,
|
|
321
|
-
detectMarket,
|
|
322
|
-
getLocalization,
|
|
323
|
-
diagnose,
|
|
324
|
-
__core: coreModules,
|
|
325
|
-
// The `<script>` build's parts register here when their file has loaded.
|
|
326
|
-
__parts: /** @type {Record<string, unknown>} */ ({}),
|
|
327
|
-
on,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
/** @typedef {typeof api} TetherApi */
|
|
331
|
-
|
|
332
|
-
export default api;
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* The plain `<script>` setup starts by itself, unless `autoMount: false` or a framework integration (Astro), which
|
|
336
|
-
* starts Tether itself.
|
|
337
|
-
*/
|
|
338
|
-
function autoStart() {
|
|
339
|
-
const globals = pageGlobals();
|
|
340
|
-
globals.Tether = api;
|
|
341
|
-
if (!globals.TETHER_CONFIG || globals.TETHER_CONFIG.autoMount === false || globals.TETHER_INTEGRATION) return;
|
|
342
|
-
const start = () => {
|
|
343
|
-
init()
|
|
344
|
-
.then(() => mount(document))
|
|
345
|
-
.catch((/** @type {unknown} */ error) => {
|
|
346
|
-
reportError(error, 'init');
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
350
|
-
else start();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (typeof window !== 'undefined' && typeof document !== 'undefined') autoStart();
|
package/src/initial.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the browser script shows first for a product, as plain data, so a server renders the same and nothing flashes
|
|
3
|
-
* when Tether takes over. Public as `initialState()` of `@occluze/tether/server`; the rules are `product-state.js`'s,
|
|
4
|
-
* the ones the browser scope uses.
|
|
5
|
-
*/
|
|
6
|
-
import { DEFAULTS } from './config.js';
|
|
7
|
-
import { initialChoice, isValueAvailable, productView } from './product-state.js';
|
|
8
|
-
import { allPlans, allocationFor, planPrice, planSavings } from './selling-plans.js';
|
|
9
|
-
/** @import { Money } from './shopify-values.js' */
|
|
10
|
-
/** @import { Product, ProductVariant } from './fragments.js' */
|
|
11
|
-
/** @import { PlanInGroup } from './selling-plans.js' */
|
|
12
|
-
/** @import { Selection, ShowCondition } from './product-state.js' */
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} InitialOptions
|
|
16
|
-
* @property {string | null | undefined} [variant] The page's `?variant=`; the first variant for sale without.
|
|
17
|
-
* @property {string | null | undefined} [sellingPlan] The page's `?selling_plan=`: subscribes on that plan.
|
|
18
|
-
* @property {string | null | undefined} [defaultPurchase] `subscription` subscribes on the first plan.
|
|
19
|
-
* @property {number | undefined} [lowStock] `low-stock` at this many or fewer.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {object} InitialPlan A plan in the plan list (`tether-plan-list`).
|
|
23
|
-
* @property {string} id
|
|
24
|
-
* @property {string} name
|
|
25
|
-
* @property {Money | null} price On this plan, for the variant.
|
|
26
|
-
* @property {number} savings In %, 0 without savings.
|
|
27
|
-
* @property {boolean} selected `is-selected`: the line is bought on this plan.
|
|
28
|
-
* @property {boolean} available The variant is on this plan; `is-unavailable` when not.
|
|
29
|
-
*/
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {object} InitialState
|
|
32
|
-
* @property {ProductVariant} variant
|
|
33
|
-
* @property {Selection} selected Option name → value of the variant: `is-selected` on those buttons.
|
|
34
|
-
* @property {'one-time' | 'subscription'} purchase
|
|
35
|
-
* @property {PlanInGroup | null} plan The plan the line is bought on, `null` when bought once.
|
|
36
|
-
* @property {InitialPlan[]} plans
|
|
37
|
-
* @property {number} savings The offered plan's savings in % (`tether-bind="subscription-savings"`), 0 without.
|
|
38
|
-
* @property {Money} price
|
|
39
|
-
* @property {Money | null} compareAtPrice Only when on sale.
|
|
40
|
-
* @property {boolean} available
|
|
41
|
-
* @property {Record<ShowCondition, boolean>} show The `tether-show` conditions: `hidden={!state.show['out-of-stock']}`.
|
|
42
|
-
* @property {Record<string, string[]>} unavailable Per option, the values without a variant for sale (given the other
|
|
43
|
-
* selected values), as plain data for frameworks that serialise props.
|
|
44
|
-
* @property {(name: string, value: string) => boolean} isUnavailable The same as a function: `is-unavailable`.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {Product | null | undefined} product
|
|
49
|
-
* @param {InitialOptions} [options]
|
|
50
|
-
* @returns {InitialState | null} `null` without a product or variants.
|
|
51
|
-
*/
|
|
52
|
-
export function initialState(product, options = {}) {
|
|
53
|
-
if (!product) return null;
|
|
54
|
-
const { variant, sellingPlan, defaultPurchase = DEFAULTS.defaultPurchase, lowStock = DEFAULTS.lowStock } = options;
|
|
55
|
-
const choice = initialChoice(product, { variant, sellingPlan, defaultPurchase });
|
|
56
|
-
const view = productView(product, { ...choice, quantity: 1 }, lowStock);
|
|
57
|
-
if (!view) return null;
|
|
58
|
-
const { selected, isSubscription } = view;
|
|
59
|
-
const isUnavailable = (/** @type {string} */ name, /** @type {string} */ value) =>
|
|
60
|
-
!isValueAvailable(product, selected, name, value);
|
|
61
|
-
return {
|
|
62
|
-
variant: view.variant,
|
|
63
|
-
selected,
|
|
64
|
-
purchase: view.purchase.kind,
|
|
65
|
-
plan: isSubscription ? view.plan : null,
|
|
66
|
-
plans: allPlans(product).map((plan) => ({
|
|
67
|
-
id: plan.id,
|
|
68
|
-
name: plan.name,
|
|
69
|
-
price: planPrice(view.variant, plan.id),
|
|
70
|
-
savings: planSavings(view.variant, plan.id).percentage,
|
|
71
|
-
selected: isSubscription && plan.id === view.purchase.planId,
|
|
72
|
-
available: Boolean(allocationFor(view.variant, plan.id)),
|
|
73
|
-
})),
|
|
74
|
-
savings: view.savings.percentage,
|
|
75
|
-
price: view.price,
|
|
76
|
-
compareAtPrice: view.compareAtPrice,
|
|
77
|
-
available: view.variant.availableForSale,
|
|
78
|
-
show: view.show,
|
|
79
|
-
unavailable: Object.fromEntries(
|
|
80
|
-
product.options.map((option) => [
|
|
81
|
-
option.name,
|
|
82
|
-
option.optionValues.map((value) => value.name).filter((value) => isUnavailable(option.name, value)),
|
|
83
|
-
]),
|
|
84
|
-
),
|
|
85
|
-
isUnavailable,
|
|
86
|
-
};
|
|
87
|
-
}
|
package/src/lazy-iife.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `<script>` build: the optional parts are separate files next to tether(.min).js (tether.markets.min.js), loaded on
|
|
3
|
-
* first use. `partsUrl` sets another folder. A part registers itself in `window.Tether.__parts` and uses the core
|
|
4
|
-
* modules in `window.Tether.__core` instead of bundling copies of them.
|
|
5
|
-
*/
|
|
6
|
-
import * as config from './config.js';
|
|
7
|
-
import * as client from './client.js';
|
|
8
|
-
import * as dom from './dom.js';
|
|
9
|
-
import * as fragments from './fragments.js';
|
|
10
|
-
import * as loader from './loader.js';
|
|
11
|
-
import * as sellingPlans from './selling-plans.js';
|
|
12
|
-
import * as status from './status.js';
|
|
13
|
-
import * as cart from './cart.js';
|
|
14
|
-
import * as product from './product.js';
|
|
15
|
-
import * as collection from './collection.js';
|
|
16
|
-
import * as mounting from './mounting.js';
|
|
17
|
-
import * as marketCodes from './market-codes.js';
|
|
18
|
-
import * as shopifyValues from './shopify-values.js';
|
|
19
|
-
import * as storeHtml from './store-html.js';
|
|
20
|
-
/** @import { Parts, PartName } from './lazy.js' */
|
|
21
|
-
|
|
22
|
-
const { cfg } = config;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The modules a part may use, shared instead of bundled twice (scripts/build.mjs maps a part's imports to these).
|
|
26
|
-
*
|
|
27
|
-
* @type {Record<string, object>}
|
|
28
|
-
*/
|
|
29
|
-
export const coreModules = {
|
|
30
|
-
'config.js': config,
|
|
31
|
-
'client.js': client,
|
|
32
|
-
'dom.js': dom,
|
|
33
|
-
'fragments.js': fragments,
|
|
34
|
-
'loader.js': loader,
|
|
35
|
-
'selling-plans.js': sellingPlans,
|
|
36
|
-
'status.js': status,
|
|
37
|
-
'cart.js': cart,
|
|
38
|
-
'product.js': product,
|
|
39
|
-
'collection.js': collection,
|
|
40
|
-
'mounting.js': mounting,
|
|
41
|
-
'market-codes.js': marketCodes,
|
|
42
|
-
'shopify-values.js': shopifyValues,
|
|
43
|
-
'store-html.js': storeHtml,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
|
|
47
|
-
const coreScript = typeof document === 'undefined' ? '' : (document.currentScript?.getAttribute('src') ?? '');
|
|
48
|
-
const coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : '';
|
|
49
|
-
|
|
50
|
-
/** @type {Map<PartName, Promise<unknown>>} Parts loaded or loading. */
|
|
51
|
-
const parts = new Map();
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The folder parts load from: `partsUrl`, else the core script's own folder. A `partsUrl` must be https or on this
|
|
55
|
-
* site, because the parts run with the same access as the core (the account part holds customer sessions).
|
|
56
|
-
*
|
|
57
|
-
* @throws {Error} For a `partsUrl` that is neither.
|
|
58
|
-
*/
|
|
59
|
-
function partsFolder() {
|
|
60
|
-
if (!cfg.partsUrl) return coreScriptUrl.replace(/[^/]*$/, '');
|
|
61
|
-
const folder = new URL(cfg.partsUrl, document.baseURI);
|
|
62
|
-
if (folder.protocol !== 'https:' && folder.origin !== window.location.origin) {
|
|
63
|
-
throw new Error(`partsUrl must be https or on this site: ${folder.href}`);
|
|
64
|
-
}
|
|
65
|
-
return folder.href;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @param {PartName} name
|
|
70
|
-
* @param {string} url
|
|
71
|
-
* @returns {Promise<unknown>}
|
|
72
|
-
*/
|
|
73
|
-
function loadScript(name, url) {
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
const script = document.createElement('script');
|
|
76
|
-
script.src = url;
|
|
77
|
-
script.async = true;
|
|
78
|
-
script.onload = () => {
|
|
79
|
-
const { Tether } = /** @type {Window & { Tether?: { __parts?: Record<string, unknown> } }} */ (window);
|
|
80
|
-
const part = Tether?.__parts?.[name];
|
|
81
|
-
if (part) resolve(part);
|
|
82
|
-
else reject(new Error(`${url} did not register`));
|
|
83
|
-
};
|
|
84
|
-
script.onerror = () => {
|
|
85
|
-
parts.delete(name);
|
|
86
|
-
reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
|
|
87
|
-
};
|
|
88
|
-
document.head.appendChild(script);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @template {PartName} Name
|
|
94
|
-
* @param {Name} name
|
|
95
|
-
* @returns {Promise<Parts[Name]>}
|
|
96
|
-
* @throws {Error} (rejected) When the part's file can't be loaded or doesn't register.
|
|
97
|
-
*/
|
|
98
|
-
export function loadPart(name) {
|
|
99
|
-
let loading = parts.get(name);
|
|
100
|
-
if (!loading) {
|
|
101
|
-
let folder;
|
|
102
|
-
try {
|
|
103
|
-
folder = partsFolder();
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
106
|
-
}
|
|
107
|
-
const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? '.min' : '';
|
|
108
|
-
loading = loadScript(name, `${folder.replace(/\/?$/, '/')}tether.${name}${suffix}.js`);
|
|
109
|
-
parts.set(name, loading);
|
|
110
|
-
}
|
|
111
|
-
// A part's file is built from the module `Parts[Name]` names (scripts/build.mjs), and registers that module.
|
|
112
|
-
return /** @type {Promise<Parts[Name]>} */ (loading);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Parts that load other parts (account → account-pages) use this same loader.
|
|
116
|
-
coreModules['lazy.js'] = { loadPart, coreModules };
|
package/src/lazy.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional parts that most pages never need, loaded on first use. In the ESM and Node builds they are dynamic
|
|
3
|
-
* imports, which bundlers split into their own chunks; the `<script>` build swaps this file for `lazy-iife.js`
|
|
4
|
-
* (scripts/build.mjs).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {{
|
|
9
|
-
* diagnose: typeof import('./diagnose.js'),
|
|
10
|
-
* markets: typeof import('./markets-ui.js'),
|
|
11
|
-
* search: typeof import('./search.js'),
|
|
12
|
-
* extras: typeof import('./extras.js'),
|
|
13
|
-
* account: typeof import('./account.js'),
|
|
14
|
-
* 'account-pages': typeof import('./account-pages.js'),
|
|
15
|
-
* analytics: typeof import('./analytics.js'),
|
|
16
|
-
* }} Parts
|
|
17
|
-
*/
|
|
18
|
-
/** @typedef {keyof Parts} PartName */
|
|
19
|
-
|
|
20
|
-
/** @type {{ [Name in PartName]: () => Promise<Parts[Name]> }} */
|
|
21
|
-
const PARTS = {
|
|
22
|
-
diagnose: () => import('./diagnose.js'),
|
|
23
|
-
markets: () => import('./markets-ui.js'),
|
|
24
|
-
search: () => import('./search.js'),
|
|
25
|
-
extras: () => import('./extras.js'),
|
|
26
|
-
account: () => import('./account.js'),
|
|
27
|
-
'account-pages': () => import('./account-pages.js'),
|
|
28
|
-
analytics: () => import('./analytics.js'),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @template {PartName} Name
|
|
33
|
-
* @param {Name} name
|
|
34
|
-
* @returns {Promise<Parts[Name]>}
|
|
35
|
-
*/
|
|
36
|
-
export const loadPart = (name) => PARTS[name]();
|
|
37
|
-
|
|
38
|
-
/** Only the `<script>` build shares its modules with separately loaded parts. */
|
|
39
|
-
export const coreModules = undefined;
|