@occluze/tether 0.6.7 → 0.6.8
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 +3 -3
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
- package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
- package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
- package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
- package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
- package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
- package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
- package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
- package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
- package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
- package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
- package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
- package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
- package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
- package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
- package/dist/esm/chunks/search-OU75RNQL.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +56 -36
- package/dist/tether.account-pages.js +773 -442
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +648 -318
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +318 -191
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +331 -242
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +78 -33
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3176 -2015
- package/dist/tether.markets.js +231 -126
- 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 +221 -126
- package/dist/tether.search.min.js +1 -1
- package/package.json +23 -4
- package/src/account/addresses-view.js +92 -0
- package/src/account/browser-session.js +359 -0
- package/src/account/markup.js +48 -0
- package/src/account/operations.js +180 -0
- package/src/account/order-lines.js +96 -0
- package/src/account/order-list.js +91 -0
- package/src/account/order-page.js +192 -0
- package/src/account/page-events.js +288 -0
- package/src/account/server-client.js +172 -0
- package/src/account/shared.js +111 -0
- package/src/account/shopify-auth.js +150 -0
- package/src/account/subscriptions-view.js +158 -0
- package/src/account-api.js +118 -0
- package/src/account-links.js +35 -0
- package/src/account-pages.js +69 -298
- package/src/account.js +280 -323
- package/src/amounts.js +36 -0
- package/src/analytics-shopify.js +241 -0
- package/src/analytics.js +304 -184
- package/src/api-version.js +75 -25
- package/src/astro.js +233 -68
- package/src/cart/checkout.js +97 -0
- package/src/cart/drawer.js +138 -0
- package/src/cart/mutations.js +234 -0
- package/src/cart/render.js +294 -0
- package/src/cart/store.js +326 -0
- package/src/cart.js +320 -488
- package/src/client.js +214 -78
- package/src/collection.js +126 -57
- package/src/config.js +323 -59
- package/src/controls/cart-controls.js +155 -0
- package/src/controls/discount.js +96 -0
- package/src/controls/extras.js +63 -0
- package/src/controls/navigation.js +83 -0
- package/src/diagnose-markup.js +333 -0
- package/src/diagnose.js +251 -202
- package/src/dom.js +219 -220
- package/src/extras.js +119 -37
- package/src/fragments.js +115 -22
- package/src/images.js +104 -0
- package/src/index.js +245 -287
- package/src/initial.js +78 -66
- package/src/lazy-iife.js +92 -22
- package/src/lazy.js +25 -4
- package/src/loader.js +263 -124
- package/src/market-codes.js +35 -0
- package/src/markets-ui.js +169 -55
- package/src/markets.js +286 -85
- package/src/markup.js +64 -0
- package/src/mounting.js +214 -91
- package/src/page-market.js +116 -0
- package/src/parts/account-pages.js +4 -1
- package/src/parts/account.js +4 -1
- package/src/parts/analytics.js +4 -1
- package/src/parts/diagnose.js +4 -1
- package/src/parts/extras.js +4 -1
- package/src/parts/markets.js +4 -1
- package/src/parts/search.js +4 -1
- package/src/presets/barba.js +120 -30
- package/src/product-events.js +130 -0
- package/src/product-render.js +257 -0
- package/src/product-state.js +318 -0
- package/src/product.js +233 -459
- package/src/repeaters.js +173 -0
- package/src/search.js +277 -107
- package/src/selling-plans.js +107 -47
- package/src/server.js +168 -205
- package/src/shopify-values.js +89 -0
- package/src/status.js +99 -39
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +199 -0
- package/src/storefront/json-ld.js +167 -0
- package/src/variant-render.js +165 -0
- package/types/astro.d.ts +13 -2
- package/types/index.d.ts +178 -27
- package/types/server.d.ts +89 -17
- package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
- package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
- package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
- package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
- package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
- package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
- package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
package/src/markets-ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Optional part, loaded on first use: market detection, select[tether-country-select]
|
|
3
|
-
*
|
|
2
|
+
* Optional part, loaded on first use: market detection, `select[tether-country-select]`, `tether-country-bind` and
|
|
3
|
+
* `Tether.setCountry()`. See markets.js for the detection rules.
|
|
4
4
|
*/
|
|
5
5
|
import { cfg, reportError } from './config.js';
|
|
6
6
|
import { TetherError } from './client.js';
|
|
@@ -9,82 +9,196 @@ import { listSelector } from './collection.js';
|
|
|
9
9
|
import { clearProductCache } from './loader.js';
|
|
10
10
|
import { attr, sel, get, emit } from './dom.js';
|
|
11
11
|
import { within, pruneLive, liveLists, liveProducts, mountedProducts, mountProduct } from './mounting.js';
|
|
12
|
-
import { rememberMarket, getLocalization, clearLocalization
|
|
12
|
+
import { rememberMarket, getLocalization, clearLocalization } from './markets.js';
|
|
13
|
+
import { toCountry, toLanguage } from './market-codes.js';
|
|
14
|
+
/** @import { LocalizedCountry, Localization } from './markets.js' */
|
|
15
|
+
/** @import { CountryCode } from './market-codes.js' */
|
|
13
16
|
|
|
14
17
|
export { detectMarket, getLocalization } from './markets.js';
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const DEFAULT_LABEL = '{name} ({currency})';
|
|
20
|
+
const DEFAULT_BIND = 'name';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} SetCountryOptions
|
|
24
|
+
* @property {string | null | undefined} [language] A Shopify language; `null` clears it. Default: the
|
|
25
|
+
* `markets.locales` entry for the country, else unchanged.
|
|
26
|
+
* @property {string | undefined} [locale] The money format. Default: the `markets.locales` entry, else unchanged.
|
|
27
|
+
* @property {boolean | undefined} [remember] Save the choice for the next pages (default true).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {LocalizedCountry} country
|
|
32
|
+
* @param {string} field A `tether-country-bind` value or a label placeholder: `name`, `code`, `currency`, `symbol`.
|
|
33
|
+
* @returns {string} `''` for any other field.
|
|
34
|
+
*/
|
|
35
|
+
function countryField(country, field) {
|
|
36
|
+
switch (field) {
|
|
37
|
+
case 'name':
|
|
38
|
+
return country.name;
|
|
39
|
+
case 'code':
|
|
40
|
+
return country.isoCode;
|
|
41
|
+
case 'currency':
|
|
42
|
+
return country.currency.isoCode;
|
|
43
|
+
case 'symbol':
|
|
44
|
+
return country.currency.symbol;
|
|
45
|
+
default:
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} template `tether-country-label`, e.g. `{name} ({currency})`.
|
|
52
|
+
* @param {LocalizedCountry} country
|
|
53
|
+
*/
|
|
54
|
+
const fillLabel = (template, country) =>
|
|
55
|
+
template.replace(/\{(name|code|currency|symbol)\}/g, (_match, /** @type {string} */ field) =>
|
|
56
|
+
countryField(country, field),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {HTMLSelectElement} select
|
|
61
|
+
* @param {LocalizedCountry[]} countries The store's countries, sorted by name.
|
|
62
|
+
* @param {string} current The country in use.
|
|
63
|
+
*/
|
|
64
|
+
function fillCountrySelect(select, countries, current) {
|
|
65
|
+
const template = select.getAttribute(attr('country-label')) || DEFAULT_LABEL;
|
|
66
|
+
// tether-countries="NL,DE,BE": only these, in that order; otherwise every country of the store.
|
|
67
|
+
const only = (select.getAttribute(attr('countries')) || '')
|
|
68
|
+
.toUpperCase()
|
|
69
|
+
.split(/[\s,]+/)
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
const listed = only.length
|
|
72
|
+
? only
|
|
73
|
+
.map((code) => countries.find((country) => country.isoCode === code))
|
|
74
|
+
.filter((country) => country !== undefined)
|
|
75
|
+
: countries;
|
|
76
|
+
select.replaceChildren(...listed.map((country) => new Option(fillLabel(template, country), country.isoCode)));
|
|
77
|
+
select.value = current;
|
|
78
|
+
// A country the store doesn't sell to selects nothing.
|
|
79
|
+
if (select.value !== current) select.selectedIndex = -1;
|
|
80
|
+
select.disabled = listed.length < 2 && !select.hasAttribute(attr('always-enabled'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {HTMLSelectElement[]} selects
|
|
85
|
+
* @param {HTMLElement[]} binds
|
|
86
|
+
* @param {Localization} localization
|
|
87
|
+
*/
|
|
88
|
+
function fillCountries(selects, binds, localization) {
|
|
89
|
+
const current = (cfg.country || localization.country.isoCode).toUpperCase();
|
|
90
|
+
const countries = [...localization.availableCountries].sort((a, b) =>
|
|
91
|
+
a.name.localeCompare(b.name, cfg.locale || undefined),
|
|
92
|
+
);
|
|
93
|
+
const inUse = countries.find((country) => country.isoCode === current) ?? localization.country;
|
|
94
|
+
selects.forEach((select) => {
|
|
95
|
+
fillCountrySelect(select, countries, current);
|
|
96
|
+
});
|
|
97
|
+
binds.forEach((el) => {
|
|
98
|
+
el.textContent = countryField(inUse, get(el, 'country-bind') || DEFAULT_BIND);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fills `select[tether-country-select]` with the countries the store sells to (Shopify Markets), and
|
|
104
|
+
* `tether-country-bind="name|code|currency|symbol"` with the current country. `tether-country-label` sets the option
|
|
105
|
+
* text from `{name}`, `{code}`, `{currency}` and `{symbol}`. A failure is reported, never thrown.
|
|
106
|
+
*
|
|
107
|
+
* @param {ParentNode} [root]
|
|
108
|
+
* @returns {Promise<void>}
|
|
20
109
|
*/
|
|
21
|
-
function renderCountrySelects(root = document) {
|
|
22
|
-
const selects = within(root, `select${sel('country-select')}`);
|
|
110
|
+
export function renderCountrySelects(root = document) {
|
|
111
|
+
const selects = /** @type {HTMLSelectElement[]} */ (within(root, `select${sel('country-select')}`));
|
|
23
112
|
const binds = within(root, sel('country-bind'));
|
|
24
113
|
if (!selects.length && !binds.length) return Promise.resolve();
|
|
25
|
-
return getLocalization()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
selects.forEach((s) => {
|
|
32
|
-
const tpl = s.getAttribute(attr('country-label')) || '{name} ({currency})';
|
|
33
|
-
// tether-countries="NL,DE,BE": only these (in your order); otherwise every country of the store.
|
|
34
|
-
const only = (s.getAttribute(attr('countries')) || '').toUpperCase().split(/[\s,]+/).filter(Boolean);
|
|
35
|
-
const list = only.length ? only.map((c) => countries.find((x) => x.isoCode === c)).filter(Boolean) : countries;
|
|
36
|
-
s.innerHTML = '';
|
|
37
|
-
list.forEach((c) => s.appendChild(new Option(fill(tpl, c), c.isoCode)));
|
|
38
|
-
s.value = current;
|
|
39
|
-
if (s.value !== current) s.selectedIndex = -1; // a country the store doesn't sell to
|
|
40
|
-
s.disabled = list.length < 2 && !s.hasAttribute(attr('always-enabled'));
|
|
114
|
+
return getLocalization()
|
|
115
|
+
.then((localization) => {
|
|
116
|
+
fillCountries(selects, binds, localization);
|
|
117
|
+
})
|
|
118
|
+
.catch((/** @type {unknown} */ error) => {
|
|
119
|
+
reportError(error, 'country select');
|
|
41
120
|
});
|
|
42
|
-
binds.forEach((el) => {
|
|
43
|
-
const k = get(el, 'country-bind') || 'name';
|
|
44
|
-
const v = { name: info?.name, code: info?.isoCode, currency: info?.currency?.isoCode, symbol: info?.currency?.symbol }[k];
|
|
45
|
-
el.textContent = v || '';
|
|
46
|
-
});
|
|
47
|
-
}).catch((err) => reportError(err, 'country select'));
|
|
48
121
|
}
|
|
49
122
|
|
|
50
123
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Tether.setCountry('DE') Tether.setCountry('BE', { language: 'FR' })
|
|
124
|
+
* @param {CountryCode} country
|
|
125
|
+
* @returns {{ country?: string, language?: string, locale?: string } | undefined} The `markets.locales` entry for it.
|
|
54
126
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!country) throw new TetherError(`Not a country code: ${code}`, { code: 'INVALID_COUNTRY' });
|
|
58
|
-
// Money format: the option, else a markets.locales entry for this country, else unchanged.
|
|
59
|
-
const entry = Object.values(cfg.markets?.locales || {}).find((m) => toCountry(m?.country) === country);
|
|
60
|
-
const own = locale || entry?.locale;
|
|
61
|
-
const loc = own || cfg.locale;
|
|
62
|
-
// Language: the option, else the markets.locales entry for this country (which may have none), else unchanged.
|
|
63
|
-
const lang = language !== undefined ? toLanguage(language) : entry ? (entry.language ? toLanguage(entry.language) : undefined) : cfg.language;
|
|
64
|
-
const changed = country !== String(cfg.country || '').toUpperCase() || lang !== cfg.language || loc !== cfg.locale;
|
|
65
|
-
cfg.country = country;
|
|
66
|
-
cfg.language = lang || undefined;
|
|
67
|
-
cfg.locale = loc;
|
|
68
|
-
if (remember) rememberMarket({ country, language: lang, locale: own });
|
|
69
|
-
if (!changed) { renderCountrySelects(); return Cart.state; }
|
|
127
|
+
const localeEntryFor = (country) =>
|
|
128
|
+
Object.values(cfg.markets?.locales ?? {}).find((entry) => toCountry(entry.country) === country);
|
|
70
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @param {string | null | undefined} language The `setCountry()` option.
|
|
132
|
+
* @param {{ language?: string } | undefined} entry The `markets.locales` entry for the country.
|
|
133
|
+
* @returns {string | null | undefined} The option, else the entry's language (which may have none), else unchanged.
|
|
134
|
+
*/
|
|
135
|
+
function languageFor(language, entry) {
|
|
136
|
+
if (language !== undefined) return toLanguage(language);
|
|
137
|
+
if (!entry) return cfg.language;
|
|
138
|
+
return entry.language ? toLanguage(entry.language) : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Loads every live product and list again in the market `cfg` now holds, and moves an existing cart to `country`.
|
|
143
|
+
*
|
|
144
|
+
* @param {CountryCode} country
|
|
145
|
+
* @returns {Promise<unknown>[]} The cart update and the list loads.
|
|
146
|
+
*/
|
|
147
|
+
function rerenderInMarket(country) {
|
|
71
148
|
clearProductCache();
|
|
149
|
+
/** @type {Promise<unknown>[]} */
|
|
72
150
|
const jobs = [];
|
|
73
|
-
if (language !== undefined) clearLocalization(); // country names follow the language
|
|
74
151
|
// Only an existing cart moves; switching country never creates one.
|
|
75
|
-
if (Cart.id())
|
|
76
|
-
|
|
152
|
+
if (Cart.id()) {
|
|
153
|
+
jobs.push(
|
|
154
|
+
Cart.setBuyerIdentity({ countryCode: country }, { create: false }).catch((/** @type {unknown} */ error) => {
|
|
155
|
+
reportError(error, 'cart country');
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
77
159
|
pruneLive();
|
|
78
160
|
liveLists.forEach((list) => jobs.push(list.load()));
|
|
79
161
|
liveProducts.forEach((el) => {
|
|
80
|
-
|
|
162
|
+
// A product inside a list is re-rendered by its list.
|
|
163
|
+
if (el.closest(listSelector())) return;
|
|
81
164
|
mountedProducts.delete(el);
|
|
82
165
|
mountProduct(el);
|
|
83
166
|
});
|
|
167
|
+
return jobs;
|
|
168
|
+
}
|
|
84
169
|
|
|
85
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Switches the market: prices, currency and translations follow (`@inContext`), the cart's
|
|
172
|
+
* `buyerIdentity.countryCode` is updated, and every product and list on the page re-renders.
|
|
173
|
+
*
|
|
174
|
+
* Tether.setCountry('DE') Tether.setCountry('BE', { language: 'FR' })
|
|
175
|
+
*
|
|
176
|
+
* @param {string} code
|
|
177
|
+
* @param {SetCountryOptions} [options]
|
|
178
|
+
* @returns {Promise<typeof Cart.state>} The cart once everything is in the new market.
|
|
179
|
+
* @throws {TetherError} `INVALID_COUNTRY` for anything that isn't a country code.
|
|
180
|
+
*/
|
|
181
|
+
export async function setCountry(code, { language, locale, remember = true } = {}) {
|
|
182
|
+
const country = toCountry(code);
|
|
183
|
+
if (!country) throw new TetherError(`Not a country code: ${code}`, { code: 'INVALID_COUNTRY' });
|
|
184
|
+
const entry = localeEntryFor(country);
|
|
185
|
+
const ownLocale = locale || entry?.locale;
|
|
186
|
+
const nextLocale = ownLocale || cfg.locale;
|
|
187
|
+
const nextLanguage = languageFor(language, entry);
|
|
188
|
+
const changed = country !== cfg.country?.toUpperCase() || nextLanguage !== cfg.language || nextLocale !== cfg.locale;
|
|
189
|
+
cfg.country = country;
|
|
190
|
+
cfg.language = nextLanguage || undefined;
|
|
191
|
+
cfg.locale = nextLocale;
|
|
192
|
+
if (remember) rememberMarket({ country, language: nextLanguage, locale: ownLocale });
|
|
193
|
+
if (!changed) {
|
|
194
|
+
void renderCountrySelects();
|
|
195
|
+
return Cart.state;
|
|
196
|
+
}
|
|
197
|
+
// Country names follow the language.
|
|
198
|
+
if (language !== undefined) clearLocalization();
|
|
199
|
+
const jobs = rerenderInMarket(country);
|
|
200
|
+
void renderCountrySelects();
|
|
86
201
|
emit('country-changed', { country, language: cfg.language });
|
|
87
202
|
await Promise.all(jobs);
|
|
88
203
|
return Cart.state;
|
|
89
204
|
}
|
|
90
|
-
|