@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/markets.js
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markets: which country and language prices, currency and translations follow.
|
|
3
|
-
*
|
|
4
|
-
* markets: {
|
|
5
|
-
* detect: true, // or a list: ['saved', 'server', 'webflow']
|
|
6
|
-
* locales: { // Webflow Localization: locale subdirectory or <html lang>
|
|
7
|
-
* de: { country: 'DE', language: 'DE', locale: 'de-DE' },
|
|
8
|
-
* 'fr-be': { country: 'BE', language: 'FR' },
|
|
9
|
-
* },
|
|
10
|
-
* cookie: 'tether_country', // a cookie your server or Worker sets (from cf-ipcountry)
|
|
11
|
-
* remember: true, // keep the visitor's tether-country-select choice
|
|
12
|
-
* }
|
|
13
|
-
*
|
|
14
|
-
* Sources, first match wins per field:
|
|
15
|
-
* saved the visitor's own choice (localStorage), from tether-country-select / Tether.setCountry()
|
|
16
|
-
* server window.TETHER_COUNTRY, <meta name="tether-country" content="DE">, or the cookie
|
|
17
|
-
* webflow locale subdirectory (/de/...) or <html lang="de-DE">, through `locales`
|
|
18
|
-
* then the static `country` / `language` config. Without `detect`, only `saved` applies, so a country select keeps
|
|
19
|
-
* working across pages; `detect: false` turns even that off. Tether never calls an IP lookup service: a country from
|
|
20
|
-
* the visitor's IP only comes from the site's own server (see README).
|
|
21
|
-
*/
|
|
22
|
-
import { cfg } from './config.js';
|
|
23
|
-
import { gql } from './client.js';
|
|
24
|
-
import { toCountry, toLanguage } from './market-codes.js';
|
|
25
|
-
/** @import { MarketsConfig } from './config.js' */
|
|
26
|
-
/** @import { CountryCode, LanguageCode } from './market-codes.js' */
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {object} Market A market a visitor chose, as `setCountry()` stores it.
|
|
30
|
-
* @property {CountryCode} country
|
|
31
|
-
* @property {LanguageCode | undefined} language
|
|
32
|
-
* @property {string | undefined} locale An `Intl` locale for money and dates.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/** @typedef {'saved' | 'server' | 'webflow'} MarketSource */
|
|
36
|
-
/** @typedef {'country' | 'language' | 'locale'} MarketField */
|
|
37
|
-
/** @typedef {Partial<Record<MarketField, MarketSource>>} MarketSources */
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {object} DetectedMarket The fields detection found, and the source each came from.
|
|
41
|
-
* @property {CountryCode} [country]
|
|
42
|
-
* @property {LanguageCode} [language]
|
|
43
|
-
* @property {string} [locale]
|
|
44
|
-
* @property {MarketSources} [sources] Absent when `detect: false` turns detection off.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @typedef {object} MarketEnv What detection reads, passed in so it runs on any document.
|
|
49
|
-
* @property {Pick<Document, 'cookie' | 'querySelector' | 'documentElement'> | null} document
|
|
50
|
-
* @property {Pick<Location, 'pathname'> | null} location
|
|
51
|
-
* @property {Pick<Storage, 'getItem'> | null} storage
|
|
52
|
-
* @property {object} global Where `TETHER_COUNTRY` and `TETHER_LANGUAGE` are read from (`window`).
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
/** @typedef {{ country?: unknown, language?: unknown, locale?: unknown }} MarketHint A source's raw values. */
|
|
56
|
-
|
|
57
|
-
/** @typedef {{ isoCode: string, name: string }} NamedCode */
|
|
58
|
-
/** @typedef {{ isoCode: string, symbol: string }} Currency */
|
|
59
|
-
/** @typedef {NamedCode & { currency: Currency }} LocalizedCountry */
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {object} Localization The store's markets, from the Storefront API's `localization`.
|
|
62
|
-
* @property {LocalizedCountry} country The country of the current `@inContext`.
|
|
63
|
-
* @property {NamedCode} language
|
|
64
|
-
* @property {(LocalizedCountry & { availableLanguages: NamedCode[] })[]} availableCountries
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
/** @type {readonly MarketSource[]} */
|
|
68
|
-
const ALL_SOURCES = ['saved', 'server', 'webflow'];
|
|
69
|
-
const DEFAULT_COOKIE = 'tether_country';
|
|
70
|
-
|
|
71
|
-
/** @returns {string} The localStorage key of the visitor's market: `<prefix>:market:<domain>`. */
|
|
72
|
-
export const storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Accessing `localStorage` throws where the browser blocks storage (sandboxed frames, cookies turned off); the visitor
|
|
76
|
-
* then simply has no saved market.
|
|
77
|
-
*
|
|
78
|
-
* @returns {Storage | null}
|
|
79
|
-
*/
|
|
80
|
-
function localStorageIfAllowed() {
|
|
81
|
-
if (typeof window === 'undefined') return null;
|
|
82
|
-
try {
|
|
83
|
-
return window.localStorage;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** @returns {MarketEnv} The page this script runs on. */
|
|
90
|
-
function pageEnv() {
|
|
91
|
-
const hasWindow = typeof window !== 'undefined';
|
|
92
|
-
return {
|
|
93
|
-
document: typeof document !== 'undefined' ? document : null,
|
|
94
|
-
location: hasWindow ? window.location : null,
|
|
95
|
-
storage: localStorageIfAllowed(),
|
|
96
|
-
global: hasWindow ? window : {},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} text
|
|
102
|
-
* @returns {unknown} The JSON value, `null` for text that isn't JSON (a key another script wrote).
|
|
103
|
-
*/
|
|
104
|
-
function parseJsonOrNull(text) {
|
|
105
|
-
try {
|
|
106
|
-
return /** @type {unknown} */ (JSON.parse(text));
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error instanceof SyntaxError) return null;
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {unknown} value
|
|
115
|
-
* @returns {Market | null} `value` as a market, when it has a valid country. A language or locale that isn't valid is
|
|
116
|
-
* left out.
|
|
117
|
-
*/
|
|
118
|
-
function toMarket(value) {
|
|
119
|
-
if (typeof value !== 'object' || value === null) return null;
|
|
120
|
-
const country = toCountry(Reflect.get(value, 'country'));
|
|
121
|
-
if (!country) return null;
|
|
122
|
-
const locale = /** @type {unknown} */ (Reflect.get(value, 'locale'));
|
|
123
|
-
return {
|
|
124
|
-
country,
|
|
125
|
-
language: toLanguage(Reflect.get(value, 'language')) ?? undefined,
|
|
126
|
-
locale: typeof locale === 'string' && locale ? locale : undefined,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The visitor's saved market (`setCountry()`, `tether-country-select`), validated: storage is written by other tabs,
|
|
132
|
-
* older versions and other scripts too.
|
|
133
|
-
*
|
|
134
|
-
* @param {Pick<Storage, 'getItem'> | null} [storage] Default `localStorage`, `null` where the browser blocks it.
|
|
135
|
-
* @returns {Market | null} `null` when nothing valid is saved.
|
|
136
|
-
*/
|
|
137
|
-
export function readSavedMarket(storage = localStorageIfAllowed()) {
|
|
138
|
-
const text = storage?.getItem(storageKey());
|
|
139
|
-
return text ? toMarket(parseJsonOrNull(text)) : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* @param {Pick<Document, 'cookie'> | null} doc
|
|
144
|
-
* @param {string} name
|
|
145
|
-
* @returns {string | null}
|
|
146
|
-
*/
|
|
147
|
-
function readCookie(doc, name) {
|
|
148
|
-
if (!doc || !name) return null;
|
|
149
|
-
const cookie = doc.cookie
|
|
150
|
-
.split(';')
|
|
151
|
-
.map((part) => part.trim())
|
|
152
|
-
.find((part) => part.startsWith(`${name}=`));
|
|
153
|
-
return cookie ? decodeURIComponent(cookie.slice(name.length + 1)) : null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* @param {MarketsConfig['locales']} locales
|
|
158
|
-
* @param {string | undefined} key
|
|
159
|
-
* @returns {MarketHint | null} The entry for `key`, ignoring case.
|
|
160
|
-
*/
|
|
161
|
-
function lookupLocale(locales, key) {
|
|
162
|
-
if (!locales || !key) return null;
|
|
163
|
-
const wanted = key.toLowerCase();
|
|
164
|
-
const found = Object.keys(locales).find((name) => name.toLowerCase() === wanted);
|
|
165
|
-
return found ? (locales[found] ?? null) : null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* @param {MarketsConfig} markets
|
|
170
|
-
* @param {MarketEnv} env
|
|
171
|
-
* @returns {MarketHint}
|
|
172
|
-
*/
|
|
173
|
-
function readServerHint(markets, env) {
|
|
174
|
-
/** @param {string} name */
|
|
175
|
-
const meta = (name) => env.document?.querySelector(`meta[name="${name}"]`)?.getAttribute('content');
|
|
176
|
-
return {
|
|
177
|
-
country:
|
|
178
|
-
Reflect.get(env.global, 'TETHER_COUNTRY') ||
|
|
179
|
-
meta('tether-country') ||
|
|
180
|
-
readCookie(env.document, markets.cookie || DEFAULT_COOKIE),
|
|
181
|
-
language: Reflect.get(env.global, 'TETHER_LANGUAGE') || meta('tether-language'),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {MarketsConfig} markets
|
|
187
|
-
* @param {MarketEnv} env
|
|
188
|
-
* @returns {MarketHint | null}
|
|
189
|
-
*/
|
|
190
|
-
function readWebflowLocale(markets, env) {
|
|
191
|
-
const segment = (env.location?.pathname ?? '/').split('/').find((part) => part !== '');
|
|
192
|
-
const fromPath = lookupLocale(markets.locales, segment);
|
|
193
|
-
if (fromPath) return fromPath;
|
|
194
|
-
const lang = env.document?.documentElement.getAttribute('lang');
|
|
195
|
-
if (!lang) return null;
|
|
196
|
-
const mapped = lookupLocale(markets.locales, lang) ?? lookupLocale(markets.locales, lang.split('-')[0]);
|
|
197
|
-
if (mapped) return { locale: lang, ...mapped };
|
|
198
|
-
if (markets.locales && markets.strict) return null;
|
|
199
|
-
// Unmapped: the language from <html lang>, a country only from a region subtag (de-CH -> CH).
|
|
200
|
-
const region = lang.split('-')[1];
|
|
201
|
-
return { language: lang, country: region?.length === 2 ? region : null, locale: lang };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {MarketSource} source
|
|
206
|
-
* @param {MarketsConfig} markets
|
|
207
|
-
* @param {MarketEnv} env
|
|
208
|
-
* @returns {MarketHint | null}
|
|
209
|
-
*/
|
|
210
|
-
function readSource(source, markets, env) {
|
|
211
|
-
switch (source) {
|
|
212
|
-
case 'saved':
|
|
213
|
-
return markets.remember === false ? null : readSavedMarket(env.storage);
|
|
214
|
-
case 'server':
|
|
215
|
-
return readServerHint(markets, env);
|
|
216
|
-
case 'webflow':
|
|
217
|
-
return readWebflowLocale(markets, env);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* @param {unknown} name
|
|
223
|
-
* @returns {name is MarketSource}
|
|
224
|
-
*/
|
|
225
|
-
const isMarketSource = (name) => ALL_SOURCES.some((source) => source === name);
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @param {MarketsConfig['detect']} detect
|
|
229
|
-
* @returns {readonly MarketSource[]} Without `detect`, only the visitor's own choice from a country select.
|
|
230
|
-
*/
|
|
231
|
-
function sourcesFor(detect) {
|
|
232
|
-
if (!detect) return ['saved'];
|
|
233
|
-
if (detect === true) return ALL_SOURCES;
|
|
234
|
-
// The config comes from the page unvalidated, so a single name or an unknown one can arrive here.
|
|
235
|
-
return /** @type {unknown[]} */ ([detect].flat()).filter(isMarketSource);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* The market the page's sources point to. Reads the DOM and storage only, so it can run on any `env`.
|
|
240
|
-
*
|
|
241
|
-
* @param {MarketsConfig | null | undefined} [markets]
|
|
242
|
-
* @param {MarketEnv} [env]
|
|
243
|
-
* @returns {DetectedMarket} Only the fields found; `{}` with `detect: false`.
|
|
244
|
-
*/
|
|
245
|
-
export function detectMarket(markets = cfg.markets, env = pageEnv()) {
|
|
246
|
-
const config = markets ?? {};
|
|
247
|
-
if (config.detect === false) return {};
|
|
248
|
-
/** @type {MarketSources} */
|
|
249
|
-
const sources = {};
|
|
250
|
-
/** @type {DetectedMarket} */
|
|
251
|
-
const found = { sources };
|
|
252
|
-
for (const source of sourcesFor(config.detect)) {
|
|
253
|
-
const hint = readSource(source, config, env);
|
|
254
|
-
if (!hint) continue;
|
|
255
|
-
const country = toCountry(hint.country);
|
|
256
|
-
const language = toLanguage(hint.language);
|
|
257
|
-
const locale = typeof hint.locale === 'string' && hint.locale ? hint.locale : null;
|
|
258
|
-
if (country && !found.country) {
|
|
259
|
-
found.country = country;
|
|
260
|
-
sources.country = source;
|
|
261
|
-
}
|
|
262
|
-
if (language && !found.language) {
|
|
263
|
-
found.language = language;
|
|
264
|
-
sources.language = source;
|
|
265
|
-
}
|
|
266
|
-
if (locale && !found.locale) {
|
|
267
|
-
found.locale = locale;
|
|
268
|
-
sources.locale = source;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return found;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* `setItem` throws when storage is full or blocked (Safari's private mode); the choice then lasts for this page only.
|
|
276
|
-
*
|
|
277
|
-
* @param {string} key
|
|
278
|
-
* @param {string} value
|
|
279
|
-
*/
|
|
280
|
-
function storeIfAllowed(key, value) {
|
|
281
|
-
try {
|
|
282
|
-
localStorage.setItem(key, value);
|
|
283
|
-
} catch {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Saves the visitor's choice for the next pages, unless `markets.remember` is false.
|
|
290
|
-
*
|
|
291
|
-
* @param {{ country: CountryCode, language?: string | null | undefined, locale?: string | null | undefined }} market
|
|
292
|
-
*/
|
|
293
|
-
export function rememberMarket(market) {
|
|
294
|
-
if (cfg.markets?.remember === false) return;
|
|
295
|
-
const saved = { country: market.country, language: market.language || undefined, locale: market.locale || undefined };
|
|
296
|
-
storeIfAllowed(storageKey(), JSON.stringify(saved));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** @type {Promise<Localization> | null} */
|
|
300
|
-
let localizationPromise = null;
|
|
301
|
-
|
|
302
|
-
/** Forgets the cached localization, for when country names change language. */
|
|
303
|
-
export function clearLocalization() {
|
|
304
|
-
localizationPromise = null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const LOCALIZATION_QUERY = `query TetherLocalization {
|
|
308
|
-
localization {
|
|
309
|
-
country { isoCode name currency { isoCode symbol } }
|
|
310
|
-
language { isoCode name }
|
|
311
|
-
availableCountries { isoCode name currency { isoCode symbol } availableLanguages { isoCode name } }
|
|
312
|
-
}
|
|
313
|
-
}`;
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* The countries the store sells to (Shopify Markets), with their currency. Cached per page; a failed request is not
|
|
317
|
-
* cached, so the next call tries again.
|
|
318
|
-
*
|
|
319
|
-
* @returns {Promise<Localization>}
|
|
320
|
-
*/
|
|
321
|
-
export function getLocalization() {
|
|
322
|
-
if (!localizationPromise) {
|
|
323
|
-
/** @type {Promise<{ localization: Localization }>} */
|
|
324
|
-
const loading = gql(LOCALIZATION_QUERY);
|
|
325
|
-
localizationPromise = loading.then(
|
|
326
|
-
(data) => data.localization,
|
|
327
|
-
(/** @type {unknown} */ error) => {
|
|
328
|
-
localizationPromise = null;
|
|
329
|
-
throw error;
|
|
330
|
-
},
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
return localizationPromise;
|
|
334
|
-
}
|
package/src/markup.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How Tether finds its markup: attribute names carry the configured prefix (`tether-product`, or `wfs-product` with
|
|
3
|
-
* `prefix: 'wfs'`), and so do the selectors and the DOM events built from them.
|
|
4
|
-
*/
|
|
5
|
-
import { cfg } from './config.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} name
|
|
9
|
-
* @returns {string} The prefixed attribute name: `product` → `tether-product`.
|
|
10
|
-
*/
|
|
11
|
-
export const attr = (name) => `${cfg.prefix}-${name}`;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} name
|
|
15
|
-
* @param {string} [value]
|
|
16
|
-
* @returns {string} A selector for the attribute, with that exact value when given.
|
|
17
|
-
*/
|
|
18
|
-
export const sel = (name, value) => (value === undefined ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {Element} el
|
|
22
|
-
* @param {string} name
|
|
23
|
-
*/
|
|
24
|
-
export const get = (el, name) => el.getAttribute(attr(name));
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {Element} el
|
|
28
|
-
* @param {string} name
|
|
29
|
-
*/
|
|
30
|
-
export const has = (el, name) => el.hasAttribute(attr(name));
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Tether's attributes go on HTML elements, so matches are typed as such. An SVG element that carries one still has
|
|
34
|
-
* the `style`, `dataset` and attribute methods Tether uses.
|
|
35
|
-
*
|
|
36
|
-
* @param {ParentNode | null | undefined} root
|
|
37
|
-
* @param {string} selector
|
|
38
|
-
* @returns {HTMLElement[]} The matches in `root`, none without a root.
|
|
39
|
-
*/
|
|
40
|
-
export function qsa(root, selector) {
|
|
41
|
-
if (!root) return [];
|
|
42
|
-
return [.../** @type {NodeListOf<HTMLElement>} */ (root.querySelectorAll(selector))];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {Element | null | undefined} root
|
|
47
|
-
* @param {string} selector
|
|
48
|
-
* @param {string} scopeSelector
|
|
49
|
-
* @returns {HTMLElement[]} Matches in `root` whose nearest `scopeSelector` ancestor is `root` itself, not a nested
|
|
50
|
-
* scope.
|
|
51
|
-
*/
|
|
52
|
-
export const scoped = (root, selector, scopeSelector) =>
|
|
53
|
-
qsa(root, selector).filter((el) => el.closest(scopeSelector) === root);
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Dispatches the bubbling event `<prefix>:<name>`.
|
|
57
|
-
*
|
|
58
|
-
* @param {string} name
|
|
59
|
-
* @param {unknown} [detail]
|
|
60
|
-
* @param {EventTarget} [target]
|
|
61
|
-
*/
|
|
62
|
-
export function emit(name, detail, target = document) {
|
|
63
|
-
target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
|
|
64
|
-
}
|
package/src/mounting.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The product scopes and lists on the page: mounting a product root, the live roots a country switch re-renders, and
|
|
3
|
-
* the products the server rendered into the page. Shared by `index.js` and the optional markets part.
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, log, warn, reportError } from './config.js';
|
|
6
|
-
import { ProductScope, scopes, rememberOptionNames } from './product.js';
|
|
7
|
-
import { listSelector } from './collection.js';
|
|
8
|
-
import { getProduct, takeStale, refreshProducts, hydrateProducts } from './loader.js';
|
|
9
|
-
import { resolveProductKey, setScopeStatus, isNotFound } from './status.js';
|
|
10
|
-
import { attr, get, qsa, emit } from './dom.js';
|
|
11
|
-
/** @import { Product } from './fragments.js' */
|
|
12
|
-
|
|
13
|
-
/** @typedef {{ root: Element, load: (more?: boolean) => Promise<unknown> }} MountedList A mounted `ListScope`. */
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The marker on the `<script type="application/json">` tags `tether/server`'s `hydrate()` writes. It is fixed, not
|
|
17
|
-
* `<prefix>-hydrate`, because the server writes it without knowing the browser's prefix.
|
|
18
|
-
*/
|
|
19
|
-
const HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
|
|
20
|
-
|
|
21
|
-
/** @type {WeakSet<Element>} */
|
|
22
|
-
export const mountedProducts = new WeakSet();
|
|
23
|
-
/** @type {WeakSet<Element>} */
|
|
24
|
-
export const mountedLists = new WeakSet();
|
|
25
|
-
/** @type {Set<HTMLElement>} Product roots to re-render after a country switch; pruned once disconnected. */
|
|
26
|
-
export const liveProducts = new Set();
|
|
27
|
-
/** @type {Set<MountedList>} Lists to reload after a country switch; pruned once disconnected. */
|
|
28
|
-
export const liveLists = new Set();
|
|
29
|
-
/** @type {WeakMap<Element, object>} Each product root's latest request, so a slow older response can't win. */
|
|
30
|
-
const latestRequest = new WeakMap();
|
|
31
|
-
|
|
32
|
-
/** Forgets roots a page transition removed, so they aren't kept alive. */
|
|
33
|
-
export function pruneLive() {
|
|
34
|
-
liveProducts.forEach((el) => {
|
|
35
|
-
if (!el.isConnected) liveProducts.delete(el);
|
|
36
|
-
});
|
|
37
|
-
liveLists.forEach((list) => {
|
|
38
|
-
if (!list.root.isConnected) liveLists.delete(list);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {ParentNode & Partial<Pick<Element, 'matches'>>} root
|
|
44
|
-
* @param {string} selector
|
|
45
|
-
* @returns {HTMLElement[]} `root` itself when it matches, then the matches inside it.
|
|
46
|
-
*/
|
|
47
|
-
export function within(root, selector) {
|
|
48
|
-
const self = root.matches?.(selector) ? [/** @type {HTMLElement} */ (root)] : [];
|
|
49
|
-
return [...self, ...qsa(root, selector)];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* `tether-page-title="{title} | My shop"` on a product root sets `document.title`, for dynamic product pages.
|
|
54
|
-
*
|
|
55
|
-
* @param {Element} el
|
|
56
|
-
* @param {Product} product
|
|
57
|
-
*/
|
|
58
|
-
function applyPageTitle(el, product) {
|
|
59
|
-
const template = get(el, 'page-title');
|
|
60
|
-
if (!template || typeof document === 'undefined') return;
|
|
61
|
-
document.title = template
|
|
62
|
-
.replace(/\{title\}/g, product.title)
|
|
63
|
-
.replace(/\{vendor\}/g, product.vendor || '')
|
|
64
|
-
.replace(/\{type\}/g, product.productType || '');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A product root without a key: on a dynamic page opened without `?handle=` (or with an empty path) it is a product
|
|
69
|
-
* that isn't there; inside a list it gets its handle when the list renders.
|
|
70
|
-
*
|
|
71
|
-
* @param {HTMLElement} el
|
|
72
|
-
* @param {{ fromUrl: boolean, raw: string }} source
|
|
73
|
-
*/
|
|
74
|
-
function mountWithoutKey(el, { fromUrl, raw }) {
|
|
75
|
-
if (fromUrl) {
|
|
76
|
-
mountedProducts.add(el);
|
|
77
|
-
setScopeStatus(el, 'not-found');
|
|
78
|
-
emit('product-not-found', { key: null, source: raw }, el);
|
|
79
|
-
} else if (!el.closest(listSelector())) {
|
|
80
|
-
warn(`[${attr('product')}] without a value. Bind it to the Shopify handle field, or use "url:handle".`, el);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* @param {HTMLElement} el
|
|
86
|
-
* @param {string} key
|
|
87
|
-
* @param {{ fromUrl: boolean, raw: string }} source
|
|
88
|
-
* @param {unknown} error
|
|
89
|
-
*/
|
|
90
|
-
function showLoadFailure(el, key, { fromUrl, raw }, error) {
|
|
91
|
-
const notFound = isNotFound(error);
|
|
92
|
-
setScopeStatus(el, notFound ? 'not-found' : 'error', error);
|
|
93
|
-
if (!notFound) {
|
|
94
|
-
reportError(error, `product ${key}`);
|
|
95
|
-
emit('error', { error, context: 'product', key }, el);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
emit('product-not-found', { key, source: raw }, el);
|
|
99
|
-
// A typo in a URL isn't worth an alert; a CMS item pointing at a deleted product is.
|
|
100
|
-
if (fromUrl) log(`product not found: ${key}`);
|
|
101
|
-
else reportError(error, `product ${key}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Mounts one product root.
|
|
106
|
-
*
|
|
107
|
-
* @param {HTMLElement} el
|
|
108
|
-
* @returns {boolean} True when a product request was started.
|
|
109
|
-
*/
|
|
110
|
-
export function mountProduct(el) {
|
|
111
|
-
if (mountedProducts.has(el)) return false;
|
|
112
|
-
const { key, ...source } = resolveProductKey(el);
|
|
113
|
-
if (!key) {
|
|
114
|
-
mountWithoutKey(el, source);
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
mountedProducts.add(el);
|
|
118
|
-
liveProducts.add(el);
|
|
119
|
-
const request = {};
|
|
120
|
-
latestRequest.set(el, request);
|
|
121
|
-
const isLatest = () => latestRequest.get(el) === request;
|
|
122
|
-
// A rendered scope keeps showing its content while it refreshes (a country switch).
|
|
123
|
-
if (scopes.get(el)) el.setAttribute(attr('state'), 'updating');
|
|
124
|
-
else setScopeStatus(el, 'loading');
|
|
125
|
-
const loading = trackMarket(getProduct(key));
|
|
126
|
-
loading
|
|
127
|
-
.then((product) => {
|
|
128
|
-
if (!isLatest()) return;
|
|
129
|
-
const existing = scopes.get(el);
|
|
130
|
-
if (existing) existing.update(product);
|
|
131
|
-
else new ProductScope(el, product);
|
|
132
|
-
applyPageTitle(el, product);
|
|
133
|
-
if (cfg.revalidate && takeStale(key)) queueRefresh(key);
|
|
134
|
-
})
|
|
135
|
-
.catch((/** @type {unknown} */ error) => {
|
|
136
|
-
if (isLatest()) showLoadFailure(el, key, source, error);
|
|
137
|
-
});
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Loads again the product scopes that failed on a network or API error, for when the browser is back online. */
|
|
142
|
-
export function retryFailed() {
|
|
143
|
-
pruneLive();
|
|
144
|
-
liveProducts.forEach((el) => {
|
|
145
|
-
if (el.getAttribute(attr('state')) !== 'error' || el.getAttribute(attr('error')) === 'not-found') return;
|
|
146
|
-
mountedProducts.delete(el);
|
|
147
|
-
mountProduct(el);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** @type {Set<string>} Server-rendered products to refresh in the next batch. */
|
|
152
|
-
let refreshQueue = new Set();
|
|
153
|
-
|
|
154
|
-
/** @param {string} key */
|
|
155
|
-
function queueRefresh(key) {
|
|
156
|
-
if (!refreshQueue.size) setTimeout(flushRefresh, 0);
|
|
157
|
-
refreshQueue.add(key);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {string} key The key the refresh was asked for.
|
|
162
|
-
* @param {Product} fresh
|
|
163
|
-
*/
|
|
164
|
-
function applyRefresh(key, fresh) {
|
|
165
|
-
liveProducts.forEach((el) => {
|
|
166
|
-
const scope = scopes.get(el);
|
|
167
|
-
if (!scope || !el.isConnected) return;
|
|
168
|
-
const rootKey = resolveProductKey(el).key;
|
|
169
|
-
if (rootKey !== key && rootKey !== fresh.handle && rootKey !== fresh.id) return;
|
|
170
|
-
if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** One batched background request refreshes every server-rendered product queued. */
|
|
175
|
-
function flushRefresh() {
|
|
176
|
-
const keys = [...refreshQueue];
|
|
177
|
-
refreshQueue = new Set();
|
|
178
|
-
refreshProducts(keys).forEach((promise, index) => {
|
|
179
|
-
const key = /** @type {string} */ (keys[index]);
|
|
180
|
-
promise
|
|
181
|
-
.then((fresh) => {
|
|
182
|
-
applyRefresh(key, fresh);
|
|
183
|
-
})
|
|
184
|
-
.catch((/** @type {unknown} */ error) => {
|
|
185
|
-
reportError(error, `refresh ${key}`);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* @type {Set<Promise<unknown>> | null} While the page's server-rendered prices are another market's: the loads to
|
|
192
|
-
* wait for before `html.tether-market-pending` goes. `null` when no market is pending.
|
|
193
|
-
*/
|
|
194
|
-
let marketPending = null;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Lists, and anything else that loads prices, hold `html.tether-market-pending` until they are in.
|
|
198
|
-
*
|
|
199
|
-
* @template T
|
|
200
|
-
* @param {Promise<T>} loading
|
|
201
|
-
* @returns {Promise<T>} `loading` itself.
|
|
202
|
-
*/
|
|
203
|
-
export const trackMarket = (loading) => {
|
|
204
|
-
marketPending?.add(loading);
|
|
205
|
-
return loading;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/** After a mount: drops `html.tether-market-pending` once this page's products are live. */
|
|
209
|
-
export function settleMarket() {
|
|
210
|
-
if (!marketPending) return;
|
|
211
|
-
const waits = [...marketPending];
|
|
212
|
-
void Promise.allSettled(waits).then(() => {
|
|
213
|
-
if (!marketPending || waits.some((wait) => !marketPending?.has(wait))) return;
|
|
214
|
-
if (waits.length !== marketPending.size) {
|
|
215
|
-
settleMarket(); // more loads came in meanwhile
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
marketPending = null;
|
|
219
|
-
document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* What `tether/server`'s `hydrate()` writes. The tag is the site's own markup, so its shape is trusted; a product
|
|
225
|
-
* without an id or a handle is skipped.
|
|
226
|
-
*
|
|
227
|
-
* @typedef {{ country?: string | null, language?: string | null, products?: (Product | null)[] }} HydratePayload
|
|
228
|
-
*/
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* @param {Product | null} product
|
|
232
|
-
* @returns {product is Product}
|
|
233
|
-
*/
|
|
234
|
-
const isUsable = (product) => Boolean(product?.id && product.handle);
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* @param {string | null | undefined} a
|
|
238
|
-
* @param {string | null | undefined} b
|
|
239
|
-
*/
|
|
240
|
-
const sameCode = (a, b) => (a ?? '').toUpperCase() === (b ?? '').toUpperCase();
|
|
241
|
-
|
|
242
|
-
/** @type {WeakSet<Element>} Hydrate tags already read. */
|
|
243
|
-
const readScripts = new WeakSet();
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Uses the products in the page's hydrate tags (`tether/server` `hydrate()`) without a request. A tag made for another
|
|
247
|
-
* market is skipped, and marks the page (`html.tether-market-pending`) until the live prices are in, so CSS can hide
|
|
248
|
-
* prices meanwhile.
|
|
249
|
-
*
|
|
250
|
-
* @param {Document | Element} [root]
|
|
251
|
-
*/
|
|
252
|
-
export function hydrateFromPage(root = document) {
|
|
253
|
-
qsa(root.ownerDocument ?? root, HYDRATE_SELECTOR).forEach((script) => {
|
|
254
|
-
if (readScripts.has(script)) return;
|
|
255
|
-
readScripts.add(script);
|
|
256
|
-
/** @type {unknown} */
|
|
257
|
-
let json;
|
|
258
|
-
try {
|
|
259
|
-
json = JSON.parse(script.textContent);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
warn('bad data-tether-hydrate', error);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const { country, language, products } = /** @type {HydratePayload | null} */ (json) ?? {};
|
|
265
|
-
const usable = Array.isArray(products) ? products.filter(isUsable) : [];
|
|
266
|
-
rememberOptionNames(usable); // the build's option names, also for other languages
|
|
267
|
-
if (sameCode(country, cfg.country) && sameCode(language, cfg.language)) {
|
|
268
|
-
hydrateProducts(usable);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
log('hydration skipped: other market');
|
|
272
|
-
if (!marketPending) {
|
|
273
|
-
marketPending = new Set();
|
|
274
|
-
document.documentElement.classList.add(`${cfg.prefix}-market-pending`);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}
|