@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
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  (() => {
2
3
  var __defProp = Object.defineProperty;
3
4
  var __export = (target, all) => {
@@ -13,7 +14,7 @@
13
14
 
14
15
  // tether-core:config.js
15
16
  var m = window.Tether.__core["config.js"];
16
- var { DEFAULTS, cfg, configure, isConfigured, log, reportError, warn } = m;
17
+ var { DEFAULTS, cfg, configure, isConfigured, log, reportError, resolveConfig, toMetafieldIdentifier, warn } = m;
17
18
 
18
19
  // tether-core:client.js
19
20
  var m2 = window.Tether.__core["client.js"];
@@ -21,156 +22,250 @@
21
22
 
22
23
  // tether-core:dom.js
23
24
  var m3 = window.Tether.__core["dom.js"];
24
- var { attr, busy, cdnUrl, dataKey, emit, formatDate, formatMoney, formatPercent, get, has, hasTemplate, imageUrl, notifyLayoutChanged, parseJSONAttr, productUrl, qsa, readText, repeat, repeatKeyed, sameImage, scoped, sel, setImage, setSelected, setText, setTextTracked, show, toAttributes, watchImages } = m3;
25
+ var { attr, busy, cdnUrl, emit, formatDate, formatMoney, formatPercent, get, has, hasTemplate, imageUrl, notifyLayoutChanged, parseJSONAttr, productUrl, qsa, repeat, repeatKeyed, sameImage, scoped, sel, setImage, setSelected, setStoreHtml, setText, setTextTracked, show, toAttributes, watchImages } = m3;
26
+
27
+ // tether-core:mounting.js
28
+ var m4 = window.Tether.__core["mounting.js"];
29
+ var { hydrateFromPage, liveLists, liveProducts, mountProduct, mountedLists, mountedProducts, pruneLive, retryFailed, settleMarket, trackMarket, within } = m4;
25
30
 
26
31
  // src/search.js
27
32
  var QUERY = `query TetherPredictive($q:String!,$limit:Int!){predictiveSearch(query:$q,limit:$limit,limitScope:EACH,types:[PRODUCT,COLLECTION,QUERY]){products{id handle title availableForSale featuredImage{url altText}priceRange{minVariantPrice{amount currencyCode}}}collections{handle title image{url altText}}queries{text}}}`;
33
+ var DEFAULT_LIMIT = 6;
34
+ var DEFAULT_MIN_CHARACTERS = 2;
35
+ var DEFAULT_ACTION = "/search";
28
36
  var mounted = /* @__PURE__ */ new WeakSet();
29
- var uid = 0;
30
- function mountSearch(root = document) {
31
- const roots = [...root.matches?.(sel("search")) ? [root] : [], ...qsa(root, sel("search"))];
32
- roots.forEach((el) => {
33
- if (!mounted.has(el)) {
34
- mounted.add(el);
35
- setup(el);
37
+ var searchBoxCount = 0;
38
+ function integerAttribute(el, name, fallback) {
39
+ return parseInt(get(el, name) || "", 10) || fallback;
40
+ }
41
+ var linkIn = (node) => node.tagName === "A" ? node : node.querySelector("a");
42
+ function bindValues(node, values) {
43
+ qsa(node, sel("search-bind")).forEach((el) => {
44
+ const field = get(el, "search-bind");
45
+ if (field === "image") setImage(el, values.image?.url, values.image?.altText || values.title);
46
+ else if (field === "title" || field === "price" || field === "text") {
47
+ if (values[field] != null) setText(el, values[field]);
36
48
  }
37
49
  });
38
50
  }
39
- function setup(rootEl) {
40
- const input = rootEl.querySelector(sel("search-input")) || rootEl.querySelector("input");
41
- const results = rootEl.querySelector(sel("search-results"));
42
- if (!input || !results) return;
43
- const limit = parseInt(get(rootEl, "search-limit") || "6", 10) || 6;
44
- const min = parseInt(get(rootEl, "search-min") || "2", 10) || 2;
45
- const id = results.id || (results.id = `${cfg.prefix}-search-${++uid}`);
46
- input.setAttribute("role", "combobox");
47
- input.setAttribute("aria-autocomplete", "list");
48
- input.setAttribute("aria-controls", id);
49
- input.setAttribute("aria-expanded", "false");
50
- input.setAttribute("autocomplete", "off");
51
- if (!results.hasAttribute("role")) results.setAttribute("role", "listbox");
52
- results.hidden = true;
53
- let token = 0;
54
- let timer = null;
55
- let last = "";
56
- const cache = /* @__PURE__ */ new Map();
57
- const setState = (state) => rootEl.setAttribute(attr("search-state"), state);
58
- const open = (yes) => {
59
- rootEl.classList.toggle("is-open", yes);
60
- results.hidden = !yes;
61
- input.setAttribute("aria-expanded", String(yes));
62
- if (!yes) input.removeAttribute("aria-activedescendant");
63
- };
64
- const items = () => qsa(results, '[role="option"]');
65
- async function run(term) {
66
- const t = ++token;
67
- if (term.length < min) {
68
- open(false);
69
- setState("idle");
51
+ function linkOption(node, href) {
52
+ const link = linkIn(node);
53
+ if (!link) return;
54
+ link.setAttribute("href", href);
55
+ link.setAttribute("role", "option");
56
+ }
57
+ var SearchBox = class {
58
+ /**
59
+ * @param {HTMLElement} root
60
+ * @param {HTMLInputElement} input
61
+ * @param {HTMLElement} results
62
+ */
63
+ constructor(root, input, results) {
64
+ this.root = root;
65
+ this.input = input;
66
+ this.results = results;
67
+ this.limit = integerAttribute(root, "search-limit", DEFAULT_LIMIT);
68
+ this.minCharacters = integerAttribute(root, "search-min", DEFAULT_MIN_CHARACTERS);
69
+ if (!results.id) {
70
+ searchBoxCount += 1;
71
+ results.id = `${cfg.prefix}-search-${searchBoxCount}`;
72
+ }
73
+ this.resultsId = results.id;
74
+ this.searchNumber = 0;
75
+ this.timer = void 0;
76
+ this.lastTerm = "";
77
+ this.cache = /* @__PURE__ */ new Map();
78
+ this.setUpAria();
79
+ this.listen();
80
+ }
81
+ setUpAria() {
82
+ this.input.setAttribute("role", "combobox");
83
+ this.input.setAttribute("aria-autocomplete", "list");
84
+ this.input.setAttribute("aria-controls", this.resultsId);
85
+ this.input.setAttribute("aria-expanded", "false");
86
+ this.input.setAttribute("autocomplete", "off");
87
+ if (!this.results.hasAttribute("role")) this.results.setAttribute("role", "listbox");
88
+ this.results.hidden = true;
89
+ }
90
+ listen() {
91
+ this.input.addEventListener("input", () => {
92
+ const term = this.input.value.trim();
93
+ if (term === this.lastTerm) return;
94
+ this.lastTerm = term;
95
+ clearTimeout(this.timer);
96
+ this.timer = setTimeout(() => void this.search(term), cfg.searchDelay);
97
+ });
98
+ this.input.addEventListener("focus", () => {
99
+ if (this.input.value.trim().length >= this.minCharacters && this.options().length) this.open(true);
100
+ });
101
+ this.input.addEventListener("keydown", (event) => {
102
+ this.onKeydown(event);
103
+ });
104
+ document.addEventListener("click", (event) => {
105
+ if (!this.root.contains(
106
+ /** @type {Node | null} */
107
+ event.target
108
+ )) this.open(false);
109
+ });
110
+ }
111
+ /** @param {SearchState} state */
112
+ setState(state) {
113
+ this.root.setAttribute(attr("search-state"), state);
114
+ }
115
+ /** @param {boolean} open */
116
+ open(open) {
117
+ this.root.classList.toggle("is-open", open);
118
+ this.results.hidden = !open;
119
+ this.input.setAttribute("aria-expanded", String(open));
120
+ if (!open) this.input.removeAttribute("aria-activedescendant");
121
+ }
122
+ options() {
123
+ return qsa(this.results, '[role="option"]');
124
+ }
125
+ /**
126
+ * @param {string} term
127
+ * @returns {Promise<SearchResults | null>}
128
+ */
129
+ async fetchResults(term) {
130
+ const cached = this.cache.get(term);
131
+ if (cached) return cached;
132
+ const data = await gql(QUERY, { q: term, limit: this.limit });
133
+ this.cache.set(term, data.predictiveSearch);
134
+ return data.predictiveSearch;
135
+ }
136
+ /** @param {string} term */
137
+ async search(term) {
138
+ this.searchNumber += 1;
139
+ const searchNumber = this.searchNumber;
140
+ if (term.length < this.minCharacters) {
141
+ this.open(false);
142
+ this.setState("idle");
70
143
  return;
71
144
  }
72
- setState("loading");
145
+ this.setState("loading");
73
146
  try {
74
- const data = cache.get(term) || (await gql(QUERY, { q: term, limit })).predictiveSearch;
75
- cache.set(term, data);
76
- if (t !== token) return;
77
- render(data, term);
78
- } catch (err) {
79
- if (t !== token) return;
80
- setState("error");
81
- reportError(err, "search");
147
+ const results = await this.fetchResults(term);
148
+ if (searchNumber === this.searchNumber) this.render(results, term);
149
+ } catch (error) {
150
+ if (searchNumber !== this.searchNumber) {
151
+ log("a superseded search failed", error);
152
+ return;
153
+ }
154
+ this.setState("error");
155
+ reportError(error, "search");
82
156
  }
83
157
  }
84
- function render(data, term) {
85
- const products = data?.products || [];
86
- const collections = data?.collections || [];
87
- const queries = (data?.queries || []).filter((q) => q.text.toLowerCase() !== term.toLowerCase());
88
- const fill = (name, list, each) => qsa(results, sel(name)).forEach((c) => {
89
- repeat(c, list, each);
90
- show(c, list.length > 0);
158
+ /**
159
+ * @template Item
160
+ * @param {string} name The list attribute, e.g. `search-products`.
161
+ * @param {readonly Item[]} items
162
+ * @param {(node: HTMLElement, item: Item) => void} render
163
+ */
164
+ fillList(name, items, render) {
165
+ qsa(this.results, sel(name)).forEach((list) => {
166
+ repeat(list, items, render);
167
+ show(list, items.length > 0);
91
168
  });
92
- fill("search-products", products, (node, p) => {
93
- binds(node, { title: p.title, price: formatMoney(p.priceRange?.minVariantPrice), image: p.featuredImage });
94
- link(node, productUrl(p.handle, p.id));
95
- node.classList.toggle("is-sold-out", !p.availableForSale);
169
+ }
170
+ /** @param {string} text */
171
+ queryUrl(text) {
172
+ const action = this.root.getAttribute("action") || DEFAULT_ACTION;
173
+ const separator = action.includes("?") ? "&" : "?";
174
+ return `${action}${separator}${this.input.name || "q"}=${encodeURIComponent(text)}`;
175
+ }
176
+ /**
177
+ * @param {SearchResults | null} results
178
+ * @param {string} term
179
+ */
180
+ render(results, term) {
181
+ const products = results?.products ?? [];
182
+ const collections = results?.collections ?? [];
183
+ const queries = (results?.queries ?? []).filter((query) => query.text.toLowerCase() !== term.toLowerCase());
184
+ this.fillList("search-products", products, (node, product) => {
185
+ const price = formatMoney(product.priceRange.minVariantPrice);
186
+ bindValues(node, { title: product.title, price, image: product.featuredImage });
187
+ linkOption(node, productUrl(product.handle, product.id));
188
+ node.classList.toggle("is-sold-out", !product.availableForSale);
96
189
  });
97
- fill("search-collections", collections, (node, c) => {
98
- binds(node, { title: c.title, image: c.image });
99
- link(node, `${(cfg.collectionPath || "/collection").replace(/\/$/, "")}/${c.handle}`);
190
+ this.fillList("search-collections", collections, (node, collection) => {
191
+ bindValues(node, { title: collection.title, image: collection.image });
192
+ linkOption(node, `${(cfg.collectionPath || "/collection").replace(/\/$/, "")}/${collection.handle}`);
100
193
  });
101
- fill("search-queries", queries, (node, q) => {
102
- binds(node, { text: q.text });
103
- const a = node.tagName === "A" ? node : node.querySelector("a");
104
- const action = rootEl.getAttribute("action") || "/search";
105
- if (a) a.setAttribute("href", `${action}${action.includes("?") ? "&" : "?"}${input.name || "q"}=${encodeURIComponent(q.text)}`);
194
+ this.fillList("search-queries", queries, (node, query) => {
195
+ bindValues(node, { text: query.text });
196
+ linkIn(node)?.setAttribute("href", this.queryUrl(query.text));
106
197
  });
107
198
  const count = products.length + collections.length + queries.length;
108
- qsa(rootEl, sel("show")).forEach((el) => {
109
- const k = get(el, "show");
110
- if (k === "search-empty") show(el, count === 0);
111
- if (k === "search-results") show(el, count > 0);
199
+ this.showCount(count);
200
+ this.setState(count ? "ready" : "empty");
201
+ this.open(true);
202
+ emit("search", { term, products, collections, queries }, this.root);
203
+ }
204
+ /** @param {number} count */
205
+ showCount(count) {
206
+ qsa(this.root, sel("show")).forEach((el) => {
207
+ const condition = get(el, "show");
208
+ if (condition === "search-empty") show(el, count === 0);
209
+ if (condition === "search-results") show(el, count > 0);
112
210
  });
113
- items().forEach((el, i) => {
114
- el.id || (el.id = `${id}-${i}`);
115
- el.setAttribute("aria-selected", "false");
211
+ this.options().forEach((option, index) => {
212
+ option.id || (option.id = `${this.resultsId}-${index}`);
213
+ option.setAttribute("aria-selected", "false");
116
214
  });
117
- setState(count ? "ready" : "empty");
118
- open(true);
119
- emit("search", { term, products, collections, queries }, rootEl);
120
215
  }
121
- function binds(node, values) {
122
- qsa(node, sel("search-bind")).forEach((el) => {
123
- const k = get(el, "search-bind");
124
- if (k === "image") setImage(el, values.image?.url, values.image?.altText || values.title);
125
- else if (values[k] != null) setText(el, values[k]);
216
+ /**
217
+ * @param {HTMLElement[]} options
218
+ * @param {number} index
219
+ */
220
+ highlight(options, index) {
221
+ options.forEach((option2, optionIndex) => {
222
+ option2.setAttribute("aria-selected", String(optionIndex === index));
126
223
  });
224
+ const option = options[index];
225
+ if (!option) return;
226
+ this.input.setAttribute("aria-activedescendant", option.id);
227
+ option.scrollIntoView?.({ block: "nearest" });
127
228
  }
128
- function link(node, href) {
129
- const a = node.tagName === "A" ? node : node.querySelector("a");
130
- if (a) {
131
- a.setAttribute("href", href);
132
- a.setAttribute("role", "option");
229
+ /** @param {KeyboardEvent} event */
230
+ onKeydown(event) {
231
+ const options = this.options();
232
+ const current = options.findIndex((option) => option.getAttribute("aria-selected") === "true");
233
+ if (event.key === "ArrowDown" && options.length) {
234
+ event.preventDefault();
235
+ this.open(true);
236
+ this.highlight(options, (current + 1) % options.length);
237
+ } else if (event.key === "ArrowUp" && options.length) {
238
+ event.preventDefault();
239
+ this.highlight(options, current <= 0 ? options.length - 1 : current - 1);
240
+ } else if (event.key === "Enter" && current >= 0) {
241
+ event.preventDefault();
242
+ options[current]?.click();
243
+ } else if (event.key === "Escape") {
244
+ this.open(false);
133
245
  }
134
246
  }
135
- input.addEventListener("input", () => {
136
- const term = input.value.trim();
137
- if (term === last) return;
138
- last = term;
139
- clearTimeout(timer);
140
- timer = setTimeout(() => run(term), cfg.searchDelay ?? 150);
141
- });
142
- input.addEventListener("focus", () => {
143
- if (input.value.trim().length >= min && items().length) open(true);
144
- });
145
- input.addEventListener("keydown", (e) => {
146
- const list = items();
147
- const cur = list.findIndex((el) => el.getAttribute("aria-selected") === "true");
148
- const move = (i) => {
149
- list.forEach((el, j) => el.setAttribute("aria-selected", String(j === i)));
150
- if (list[i]) {
151
- input.setAttribute("aria-activedescendant", list[i].id);
152
- list[i].scrollIntoView?.({ block: "nearest" });
153
- }
154
- };
155
- if (e.key === "ArrowDown" && list.length) {
156
- e.preventDefault();
157
- open(true);
158
- move((cur + 1) % list.length);
159
- } else if (e.key === "ArrowUp" && list.length) {
160
- e.preventDefault();
161
- move(cur <= 0 ? list.length - 1 : cur - 1);
162
- } else if (e.key === "Enter" && cur >= 0) {
163
- e.preventDefault();
164
- list[cur].click();
165
- } else if (e.key === "Escape") {
166
- open(false);
167
- }
168
- });
169
- document.addEventListener("click", (e) => {
170
- if (!rootEl.contains(e.target)) open(false);
171
- });
247
+ };
248
+ function mountSearch(root = document) {
249
+ for (const el of within(root, sel("search"))) {
250
+ if (mounted.has(el)) continue;
251
+ mounted.add(el);
252
+ const input = (
253
+ /** @type {HTMLInputElement | null} */
254
+ el.querySelector(sel("search-input")) ?? el.querySelector("input")
255
+ );
256
+ const results = (
257
+ /** @type {HTMLElement | null} */
258
+ el.querySelector(sel("search-results"))
259
+ );
260
+ if (input && results) new SearchBox(el, input, results);
261
+ }
172
262
  }
173
263
 
174
264
  // src/parts/search.js
175
- window.Tether.__parts.search = search_exports;
265
+ var { Tether } = (
266
+ /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */
267
+ window
268
+ );
269
+ if (!Tether) throw new Error("tether.search.js runs after the Tether script, which loads it");
270
+ Tether.__parts["search"] = search_exports;
176
271
  })();
@@ -1 +1 @@
1
- (()=>{var Q=Object.defineProperty;var Y=(n,s)=>{for(var c in s)Q(n,c,{get:s[c],enumerable:!0})};var S={};Y(S,{mountSearch:()=>G});var H=window.Tether.__core["config.js"],{DEFAULTS:ee,cfg:A,configure:te,isConfigured:re,log:ie,reportError:E,warn:ae}=H;var J=window.Tether.__core["client.js"],{TetherError:ne,endpoint:ce,gql:U,request:oe,unwrap:le,withContext:ue}=J;var W=window.Tether.__core["dom.js"],{attr:D,busy:he,cdnUrl:fe,dataKey:me,emit:_,formatDate:pe,formatMoney:P,formatPercent:ge,get:g,has:be,hasTemplate:we,imageUrl:Ae,notifyLayoutChanged:ye,parseJSONAttr:xe,productUrl:R,qsa:h,readText:Te,repeat:N,repeatKeyed:qe,sameImage:Se,scoped:$e,sel:u,setImage:O,setSelected:ke,setText:j,setTextTracked:ve,show:y,toAttributes:Ie,watchImages:Le}=W;var z="query TetherPredictive($q:String!,$limit:Int!){predictiveSearch(query:$q,limit:$limit,limitScope:EACH,types:[PRODUCT,COLLECTION,QUERY]){products{id handle title availableForSale featuredImage{url altText}priceRange{minVariantPrice{amount currencyCode}}}collections{handle title image{url altText}}queries{text}}}",F=new WeakSet,B=0;function G(n=document){[...n.matches?.(u("search"))?[n]:[],...h(n,u("search"))].forEach(c=>{F.has(c)||(F.add(c),X(c))})}function X(n){let s=n.querySelector(u("search-input"))||n.querySelector("input"),c=n.querySelector(u("search-results"));if(!s||!c)return;let M=parseInt(g(n,"search-limit")||"6",10)||6,$=parseInt(g(n,"search-min")||"2",10)||2,k=c.id||(c.id=`${A.prefix}-search-${++B}`);s.setAttribute("role","combobox"),s.setAttribute("aria-autocomplete","list"),s.setAttribute("aria-controls",k),s.setAttribute("aria-expanded","false"),s.setAttribute("autocomplete","off"),c.hasAttribute("role")||c.setAttribute("role","listbox"),c.hidden=!0;let x=0,v=null,I="",L=new Map,b=e=>n.setAttribute(D("search-state"),e),d=e=>{n.classList.toggle("is-open",e),c.hidden=!e,s.setAttribute("aria-expanded",String(e)),e||s.removeAttribute("aria-activedescendant")},T=()=>h(c,'[role="option"]');async function V(e){let t=++x;if(e.length<$){d(!1),b("idle");return}b("loading");try{let i=L.get(e)||(await U(z,{q:e,limit:M})).predictiveSearch;if(L.set(e,i),t!==x)return;K(i,e)}catch(i){if(t!==x)return;b("error"),E(i,"search")}}function K(e,t){let i=e?.products||[],o=e?.collections||[],l=(e?.queries||[]).filter(r=>r.text.toLowerCase()!==t.toLowerCase()),f=(r,a,w)=>h(c,u(r)).forEach(p=>{N(p,a,w),y(p,a.length>0)});f("search-products",i,(r,a)=>{q(r,{title:a.title,price:P(a.priceRange?.minVariantPrice),image:a.featuredImage}),C(r,R(a.handle,a.id)),r.classList.toggle("is-sold-out",!a.availableForSale)}),f("search-collections",o,(r,a)=>{q(r,{title:a.title,image:a.image}),C(r,`${(A.collectionPath||"/collection").replace(/\/$/,"")}/${a.handle}`)}),f("search-queries",l,(r,a)=>{q(r,{text:a.text});let w=r.tagName==="A"?r:r.querySelector("a"),p=n.getAttribute("action")||"/search";w&&w.setAttribute("href",`${p}${p.includes("?")?"&":"?"}${s.name||"q"}=${encodeURIComponent(a.text)}`)});let m=i.length+o.length+l.length;h(n,u("show")).forEach(r=>{let a=g(r,"show");a==="search-empty"&&y(r,m===0),a==="search-results"&&y(r,m>0)}),T().forEach((r,a)=>{r.id||(r.id=`${k}-${a}`),r.setAttribute("aria-selected","false")}),b(m?"ready":"empty"),d(!0),_("search",{term:t,products:i,collections:o,queries:l},n)}function q(e,t){h(e,u("search-bind")).forEach(i=>{let o=g(i,"search-bind");o==="image"?O(i,t.image?.url,t.image?.altText||t.title):t[o]!=null&&j(i,t[o])})}function C(e,t){let i=e.tagName==="A"?e:e.querySelector("a");i&&(i.setAttribute("href",t),i.setAttribute("role","option"))}s.addEventListener("input",()=>{let e=s.value.trim();e!==I&&(I=e,clearTimeout(v),v=setTimeout(()=>V(e),A.searchDelay??150))}),s.addEventListener("focus",()=>{s.value.trim().length>=$&&T().length&&d(!0)}),s.addEventListener("keydown",e=>{let t=T(),i=t.findIndex(l=>l.getAttribute("aria-selected")==="true"),o=l=>{t.forEach((f,m)=>f.setAttribute("aria-selected",String(m===l))),t[l]&&(s.setAttribute("aria-activedescendant",t[l].id),t[l].scrollIntoView?.({block:"nearest"}))};e.key==="ArrowDown"&&t.length?(e.preventDefault(),d(!0),o((i+1)%t.length)):e.key==="ArrowUp"&&t.length?(e.preventDefault(),o(i<=0?t.length-1:i-1)):e.key==="Enter"&&i>=0?(e.preventDefault(),t[i].click()):e.key==="Escape"&&d(!1)}),document.addEventListener("click",e=>{n.contains(e.target)||d(!1)})}window.Tether.__parts.search=S;})();
1
+ "use strict";(()=>{var N=Object.defineProperty;var P=(s,t)=>{for(var e in t)N(s,e,{get:t[e],enumerable:!0})};var p={};P(p,{mountSearch:()=>Q});var R=window.Tether.__core["config.js"],{DEFAULTS:J,cfg:l,configure:W,isConfigured:z,log:w,reportError:b,resolveConfig:B,toMetafieldIdentifier:G,warn:X}=R;var F=window.Tether.__core["client.js"],{TetherError:tt,endpoint:et,gql:A,request:it,unwrap:st,withContext:rt}=F;var M=window.Tether.__core["dom.js"],{attr:y,busy:at,cdnUrl:ot,emit:T,formatDate:ht,formatMoney:E,formatPercent:ct,get:u,has:lt,hasTemplate:ut,imageUrl:dt,notifyLayoutChanged:mt,parseJSONAttr:ft,productUrl:S,qsa:c,repeat:x,repeatKeyed:pt,sameImage:gt,scoped:wt,sel:o,setImage:L,setSelected:bt,setStoreHtml:At,setText:C,setTextTracked:yt,show:d,toAttributes:Tt,watchImages:Et}=M;var O=window.Tether.__core["mounting.js"],{hydrateFromPage:xt,liveLists:Lt,liveProducts:Ct,mountProduct:It,mountedLists:qt,mountedProducts:Ut,pruneLive:kt,retryFailed:$t,settleMarket:_t,trackMarket:vt,within:I}=O;var j="query TetherPredictive($q:String!,$limit:Int!){predictiveSearch(query:$q,limit:$limit,limitScope:EACH,types:[PRODUCT,COLLECTION,QUERY]){products{id handle title availableForSale featuredImage{url altText}priceRange{minVariantPrice{amount currencyCode}}}collections{handle title image{url altText}}queries{text}}}",V=6,H=2,K="/search",q=new WeakSet,U=0;function k(s,t,e){return parseInt(u(s,t)||"",10)||e}var _=s=>s.tagName==="A"?s:s.querySelector("a");function m(s,t){c(s,o("search-bind")).forEach(e=>{let i=u(e,"search-bind");i==="image"?L(e,t.image?.url,t.image?.altText||t.title):(i==="title"||i==="price"||i==="text")&&t[i]!=null&&C(e,t[i])})}function $(s,t){let e=_(s);e&&(e.setAttribute("href",t),e.setAttribute("role","option"))}var f=class{constructor(t,e,i){this.root=t,this.input=e,this.results=i,this.limit=k(t,"search-limit",V),this.minCharacters=k(t,"search-min",H),i.id||(U+=1,i.id=`${l.prefix}-search-${U}`),this.resultsId=i.id,this.searchNumber=0,this.timer=void 0,this.lastTerm="",this.cache=new Map,this.setUpAria(),this.listen()}setUpAria(){this.input.setAttribute("role","combobox"),this.input.setAttribute("aria-autocomplete","list"),this.input.setAttribute("aria-controls",this.resultsId),this.input.setAttribute("aria-expanded","false"),this.input.setAttribute("autocomplete","off"),this.results.hasAttribute("role")||this.results.setAttribute("role","listbox"),this.results.hidden=!0}listen(){this.input.addEventListener("input",()=>{let t=this.input.value.trim();t!==this.lastTerm&&(this.lastTerm=t,clearTimeout(this.timer),this.timer=setTimeout(()=>void this.search(t),l.searchDelay))}),this.input.addEventListener("focus",()=>{this.input.value.trim().length>=this.minCharacters&&this.options().length&&this.open(!0)}),this.input.addEventListener("keydown",t=>{this.onKeydown(t)}),document.addEventListener("click",t=>{this.root.contains(t.target)||this.open(!1)})}setState(t){this.root.setAttribute(y("search-state"),t)}open(t){this.root.classList.toggle("is-open",t),this.results.hidden=!t,this.input.setAttribute("aria-expanded",String(t)),t||this.input.removeAttribute("aria-activedescendant")}options(){return c(this.results,'[role="option"]')}async fetchResults(t){let e=this.cache.get(t);if(e)return e;let i=await A(j,{q:t,limit:this.limit});return this.cache.set(t,i.predictiveSearch),i.predictiveSearch}async search(t){this.searchNumber+=1;let e=this.searchNumber;if(t.length<this.minCharacters){this.open(!1),this.setState("idle");return}this.setState("loading");try{let i=await this.fetchResults(t);e===this.searchNumber&&this.render(i,t)}catch(i){if(e!==this.searchNumber){w("a superseded search failed",i);return}this.setState("error"),b(i,"search")}}fillList(t,e,i){c(this.results,o(t)).forEach(a=>{x(a,e,i),d(a,e.length>0)})}queryUrl(t){let e=this.root.getAttribute("action")||K,i=e.includes("?")?"&":"?";return`${e}${i}${this.input.name||"q"}=${encodeURIComponent(t)}`}render(t,e){let i=t?.products??[],a=t?.collections??[],h=(t?.queries??[]).filter(n=>n.text.toLowerCase()!==e.toLowerCase());this.fillList("search-products",i,(n,r)=>{let D=E(r.priceRange.minVariantPrice);m(n,{title:r.title,price:D,image:r.featuredImage}),$(n,S(r.handle,r.id)),n.classList.toggle("is-sold-out",!r.availableForSale)}),this.fillList("search-collections",a,(n,r)=>{m(n,{title:r.title,image:r.image}),$(n,`${(l.collectionPath||"/collection").replace(/\/$/,"")}/${r.handle}`)}),this.fillList("search-queries",h,(n,r)=>{m(n,{text:r.text}),_(n)?.setAttribute("href",this.queryUrl(r.text))});let g=i.length+a.length+h.length;this.showCount(g),this.setState(g?"ready":"empty"),this.open(!0),T("search",{term:e,products:i,collections:a,queries:h},this.root)}showCount(t){c(this.root,o("show")).forEach(e=>{let i=u(e,"show");i==="search-empty"&&d(e,t===0),i==="search-results"&&d(e,t>0)}),this.options().forEach((e,i)=>{e.id||(e.id=`${this.resultsId}-${i}`),e.setAttribute("aria-selected","false")})}highlight(t,e){t.forEach((a,h)=>{a.setAttribute("aria-selected",String(h===e))});let i=t[e];i&&(this.input.setAttribute("aria-activedescendant",i.id),i.scrollIntoView?.({block:"nearest"}))}onKeydown(t){let e=this.options(),i=e.findIndex(a=>a.getAttribute("aria-selected")==="true");t.key==="ArrowDown"&&e.length?(t.preventDefault(),this.open(!0),this.highlight(e,(i+1)%e.length)):t.key==="ArrowUp"&&e.length?(t.preventDefault(),this.highlight(e,i<=0?e.length-1:i-1)):t.key==="Enter"&&i>=0?(t.preventDefault(),e[i]?.click()):t.key==="Escape"&&this.open(!1)}};function Q(s=document){for(let t of I(s,o("search"))){if(q.has(t))continue;q.add(t);let e=t.querySelector(o("search-input"))??t.querySelector("input"),i=t.querySelector(o("search-results"));e&&i&&new f(t,e,i)}}var{Tether:v}=window;if(!v)throw new Error("tether.search.js runs after the Tether script, which loads it");v.__parts.search=p;})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occluze/tether",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Free, open-source alternative to Smootify and Storesynk (Shopyflow): Shopify products, cart and subscriptions in Webflow with HTML attributes. Optional Shopify to Webflow CMS sync.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -37,20 +37,39 @@
37
37
  "scripts": {
38
38
  "build": "node scripts/build.mjs",
39
39
  "dev": "node scripts/build.mjs --watch",
40
+ "format": "prettier --write .",
41
+ "format:check": "prettier --check .",
42
+ "lint": "eslint .",
43
+ "typecheck": "tsc -p tsconfig.json",
44
+ "knip": "knip",
45
+ "deps": "depcruise src server/src server/bin server/scripts scripts",
46
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm run knip && npm run deps && npm test",
47
+ "prepare": "lefthook install",
40
48
  "test": "node --test test/*.test.js server/test/*.test.js && npm run test:types",
49
+ "test:types": "tsc -p types/tsconfig.json",
41
50
  "smoke": "node scripts/smoke.mjs",
42
- "pretest": "node scripts/build.mjs",
51
+ "pretest": "node scripts/build.mjs --out .test-dist",
43
52
  "presmoke": "node scripts/build.mjs",
44
- "test:types": "tsc -p types/tsconfig.json",
45
53
  "release": "node scripts/release.mjs",
46
54
  "e2e:build": "npm run build && cd examples/astro && (test -f .env || cp .env.example .env) && npm install --no-audit --no-fund && npm install --no-save ../.. && npx astro build",
47
55
  "e2e": "npm run e2e:build && playwright test -c e2e/playwright.config.js"
48
56
  },
49
57
  "devDependencies": {
58
+ "@eslint/js": "10.0.1",
50
59
  "@playwright/test": "^1.64.1",
60
+ "@tsconfig/strictest": "2.0.8",
61
+ "@types/node": "24.19.2",
62
+ "dependency-cruiser": "18.7.2",
51
63
  "esbuild": "^0.25.0",
64
+ "eslint": "10.14.0",
65
+ "eslint-config-prettier": "10.1.8",
66
+ "globals": "17.14.0",
52
67
  "jsdom": "^26.0.0",
53
- "typescript": "^5.9.3"
68
+ "knip": "6.44.1",
69
+ "lefthook": "2.2.2",
70
+ "prettier": "3.9.10",
71
+ "typescript": "npm:@typescript/typescript6@6.0.2",
72
+ "typescript-eslint": "8.72.1"
54
73
  },
55
74
  "license": "MIT",
56
75
  "author": {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The customer's addresses (`tether-addresses`), with `tether-show="addresses-loading"`, `addresses-empty` and
3
+ * `addresses-error`. The last list rendered is kept, so an edit button can fill the address form from it.
4
+ */
5
+ import { reportError } from '../config.js';
6
+ import { get, qsa, repeat, sel, setText } from '../dom.js';
7
+ import { addressText, bindsIn, ownValue, showIn } from './markup.js';
8
+ import { addresses } from './operations.js';
9
+
10
+ /**
11
+ * @typedef {object} CustomerAddress An address of account-ops.js's documents, with `isDefault` added.
12
+ * @property {string} id
13
+ * @property {string | null} firstName
14
+ * @property {string | null} lastName
15
+ * @property {string | null} company
16
+ * @property {string | null} address1
17
+ * @property {string | null} address2
18
+ * @property {string | null} city
19
+ * @property {string | null} province
20
+ * @property {string | null} zoneCode
21
+ * @property {string | null} zip
22
+ * @property {string | null} country
23
+ * @property {string | null} countryCode
24
+ * @property {string | null} phoneNumber
25
+ * @property {string[]} formatted
26
+ * @property {boolean} isDefault
27
+ */
28
+
29
+ /** @type {readonly CustomerAddress[]} */
30
+ let shown = [];
31
+
32
+ /**
33
+ * @param {string} id
34
+ * @returns {CustomerAddress | undefined} The address with this id in the list shown last.
35
+ */
36
+ export const shownAddress = (id) => shown.find((address) => address.id === id);
37
+
38
+ /**
39
+ * @param {CustomerAddress} address
40
+ * @returns {Record<string, string>} The text of each `tether-address-bind` value.
41
+ */
42
+ const addressTexts = (address) => ({
43
+ formatted: addressText(address),
44
+ name: [address.firstName, address.lastName].filter(Boolean).join(' '),
45
+ company: address.company || '',
46
+ address1: address.address1 || '',
47
+ address2: address.address2 || '',
48
+ city: address.city || '',
49
+ zip: address.zip || '',
50
+ province: address.province || '',
51
+ country: address.country || '',
52
+ phone: address.phoneNumber || '',
53
+ });
54
+
55
+ /**
56
+ * @param {HTMLElement} row
57
+ * @param {CustomerAddress} address
58
+ */
59
+ function fillAddress(row, address) {
60
+ row.dataset['tetherAddress'] = address.id;
61
+ const texts = addressTexts(address);
62
+ bindsIn(row, 'address-bind').forEach((el) => {
63
+ const text = ownValue(texts, get(el, 'address-bind') ?? '');
64
+ if (text !== undefined) setText(el, text);
65
+ });
66
+ showIn(row, 'address-show', { default: address.isDefault, 'not-default': !address.isDefault });
67
+ }
68
+
69
+ /**
70
+ * Loads and renders the addresses. A failure is reported and shows `addresses-error`, never "no addresses yet".
71
+ *
72
+ * @param {ParentNode} root
73
+ */
74
+ export async function renderAddresses(root) {
75
+ const boxes = qsa(root, sel('addresses'));
76
+ if (!boxes.length) return;
77
+ showIn(root, 'show', { 'addresses-loading': true, 'addresses-empty': false });
78
+ /** @type {CustomerAddress[]} */
79
+ let list;
80
+ try {
81
+ list = await addresses();
82
+ } catch (error) {
83
+ reportError(error, 'addresses');
84
+ showIn(root, 'show', { 'addresses-loading': false, 'addresses-error': true });
85
+ return;
86
+ }
87
+ shown = list;
88
+ showIn(root, 'show', { 'addresses-loading': false, 'addresses-empty': list.length === 0, 'addresses-error': false });
89
+ boxes.forEach((box) => {
90
+ repeat(box, list, fillAddress);
91
+ });
92
+ }