@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.
Files changed (135) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
  4. package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
  5. package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
  6. package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
  7. package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
  8. package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
  9. package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
  10. package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
  11. package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
  12. package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
  13. package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
  14. package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
  15. package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
  16. package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
  17. package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
  18. package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
  19. package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
  20. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
  21. package/dist/esm/chunks/search-OU75RNQL.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -1
  24. package/dist/presets/barba.esm.js +56 -36
  25. package/dist/tether.account-pages.js +773 -442
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +648 -318
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +318 -191
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +331 -242
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +78 -33
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3176 -2015
  36. package/dist/tether.markets.js +231 -126
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +221 -126
  41. package/dist/tether.search.min.js +1 -1
  42. package/package.json +23 -4
  43. package/src/account/addresses-view.js +92 -0
  44. package/src/account/browser-session.js +359 -0
  45. package/src/account/markup.js +48 -0
  46. package/src/account/operations.js +180 -0
  47. package/src/account/order-lines.js +96 -0
  48. package/src/account/order-list.js +91 -0
  49. package/src/account/order-page.js +192 -0
  50. package/src/account/page-events.js +288 -0
  51. package/src/account/server-client.js +172 -0
  52. package/src/account/shared.js +111 -0
  53. package/src/account/shopify-auth.js +150 -0
  54. package/src/account/subscriptions-view.js +158 -0
  55. package/src/account-api.js +118 -0
  56. package/src/account-links.js +35 -0
  57. package/src/account-pages.js +69 -298
  58. package/src/account.js +280 -323
  59. package/src/amounts.js +36 -0
  60. package/src/analytics-shopify.js +241 -0
  61. package/src/analytics.js +304 -184
  62. package/src/api-version.js +75 -25
  63. package/src/astro.js +233 -68
  64. package/src/cart/checkout.js +97 -0
  65. package/src/cart/drawer.js +138 -0
  66. package/src/cart/mutations.js +234 -0
  67. package/src/cart/render.js +294 -0
  68. package/src/cart/store.js +326 -0
  69. package/src/cart.js +320 -488
  70. package/src/client.js +214 -78
  71. package/src/collection.js +126 -57
  72. package/src/config.js +323 -59
  73. package/src/controls/cart-controls.js +155 -0
  74. package/src/controls/discount.js +96 -0
  75. package/src/controls/extras.js +63 -0
  76. package/src/controls/navigation.js +83 -0
  77. package/src/diagnose-markup.js +333 -0
  78. package/src/diagnose.js +251 -202
  79. package/src/dom.js +219 -220
  80. package/src/extras.js +119 -37
  81. package/src/fragments.js +115 -22
  82. package/src/images.js +104 -0
  83. package/src/index.js +245 -287
  84. package/src/initial.js +78 -66
  85. package/src/lazy-iife.js +92 -22
  86. package/src/lazy.js +25 -4
  87. package/src/loader.js +263 -124
  88. package/src/market-codes.js +35 -0
  89. package/src/markets-ui.js +169 -55
  90. package/src/markets.js +286 -85
  91. package/src/markup.js +64 -0
  92. package/src/mounting.js +214 -91
  93. package/src/page-market.js +116 -0
  94. package/src/parts/account-pages.js +4 -1
  95. package/src/parts/account.js +4 -1
  96. package/src/parts/analytics.js +4 -1
  97. package/src/parts/diagnose.js +4 -1
  98. package/src/parts/extras.js +4 -1
  99. package/src/parts/markets.js +4 -1
  100. package/src/parts/search.js +4 -1
  101. package/src/presets/barba.js +120 -30
  102. package/src/product-events.js +130 -0
  103. package/src/product-render.js +257 -0
  104. package/src/product-state.js +318 -0
  105. package/src/product.js +233 -459
  106. package/src/repeaters.js +173 -0
  107. package/src/search.js +277 -107
  108. package/src/selling-plans.js +107 -47
  109. package/src/server.js +168 -205
  110. package/src/shopify-values.js +89 -0
  111. package/src/status.js +99 -39
  112. package/src/store-html.js +0 -0
  113. package/src/storefront/catalog.js +199 -0
  114. package/src/storefront/json-ld.js +167 -0
  115. package/src/variant-render.js +165 -0
  116. package/types/astro.d.ts +13 -2
  117. package/types/index.d.ts +178 -27
  118. package/types/server.d.ts +89 -17
  119. package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
  120. package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
  121. package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
  122. package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
  123. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  124. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  125. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  126. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  127. package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
  128. package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
  129. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  130. package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
  131. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  132. package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
  133. package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
  134. package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
  135. 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
- * tether-country-bind and Tether.setCountry(). See markets.js for the detection rules.
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, toCountry, toLanguage } from './markets.js';
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
- export /**
17
- * select[tether-country-select]: countries the store sells to (Shopify Markets).
18
- * tether-country-label="{name} ({currency})" sets the option text ({name}, {code}, {currency}, {symbol}).
19
- * tether-country-bind="name|code|currency|symbol" shows the current country anywhere.
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().then((loc) => {
26
- const current = (cfg.country || loc.country?.isoCode || '').toUpperCase();
27
- const countries = [...(loc.availableCountries || [])].sort((a, b) => a.name.localeCompare(b.name, cfg.locale || undefined));
28
- const info = countries.find((c) => c.isoCode === current) || loc.country;
29
- const fill = (tpl, c) => tpl.replace(/\{name\}/g, c.name).replace(/\{code\}/g, c.isoCode)
30
- .replace(/\{currency\}/g, c.currency?.isoCode || '').replace(/\{symbol\}/g, c.currency?.symbol || '');
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
- * Switches the market: prices, currency and translations follow (@inContext), the cart's
52
- * buyerIdentity.countryCode is updated, and every product and list on the page re-renders.
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
- export async function setCountry(code, { language, locale, remember = true } = {}) {
56
- const country = toCountry(code);
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()) jobs.push(Cart.setBuyerIdentity({ countryCode: country }, { create: false }).catch((err) => reportError(err, 'cart country')));
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
- if (el.closest(listSelector())) return; // re-rendered by its list
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
- renderCountrySelects();
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
-