@occluze/tether 0.6.6 → 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 +776 -432
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +655 -321
- 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 +334 -243
- 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 +3171 -1998
- 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 +68 -284
- package/src/account.js +281 -317
- 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 +322 -481
- 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 -285
- 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-QUBHQG3C.js +0 -1
- package/dist/esm/chunks/account-pages-VWDHF6WA.js +0 -1
- package/dist/esm/chunks/analytics-SLATWPLC.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-L2TRRKYV.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/chunk-VCPI7MJ2.js +0 -1
- package/dist/esm/chunks/chunk-VPVJPNB4.js +0 -1
- package/dist/esm/chunks/chunk-ZIUIWEON.js +0 -1
- package/dist/esm/chunks/diagnose-VXQBSD3A.js +0 -14
- package/dist/esm/chunks/extras-R4X34VQL.js +0 -1
- package/dist/esm/chunks/markets-ui-F3R5T5EL.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
package/dist/tether.search.js
CHANGED
|
@@ -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,
|
|
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
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|