Skip to content

Commit 808bcdc

Browse files
committed
wip
1 parent f9e3557 commit 808bcdc

File tree

2 files changed

+69
-30
lines changed

2 files changed

+69
-30
lines changed

admin/app/javascript/solidus_admin/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,19 @@ export const setValidity = (element, error) => {
3030

3131
element.addEventListener(clearOn, clearValidity, { once: true });
3232
};
33+
34+
export function parseLinkHeader(header) {
35+
if (!header) return {};
36+
37+
const links = {};
38+
header.split(",").forEach((link) => {
39+
// match against something like this '<https://example.com>; rel="next"'
40+
const match = link.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/);
41+
if (match) {
42+
const [, url, rel] = match;
43+
links[rel] = url;
44+
}
45+
});
46+
47+
return links;
48+
}

admin/app/javascript/solidus_admin/web_components/solidus_select.js

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import TomSelect from "tom-select";
2-
import { setValidity } from "solidus_admin/utils";
2+
import { setValidity, parseLinkHeader } from "solidus_admin/utils";
33

44
class SolidusSelect extends HTMLSelectElement {
55
static observedAttributes = ["synced"];
@@ -21,7 +21,7 @@ class SolidusSelect extends HTMLSelectElement {
2121

2222
this.setAttribute("hidden", "true");
2323
this.setAttribute("aria-hidden", "true");
24-
24+
this.fixDropdownScroll();
2525
setValidity(this, this.dataset.errorMessage);
2626
}
2727

@@ -46,7 +46,7 @@ class SolidusSelect extends HTMLSelectElement {
4646
dropdownContentClass: "dropdown-content",
4747
optionClass: "option",
4848
wrapperClass: "wrapper",
49-
maxOptions: 500,
49+
maxOptions: null,
5050
refreshThrottle: 0,
5151
plugins: {
5252
no_active_items: true,
@@ -55,18 +55,6 @@ class SolidusSelect extends HTMLSelectElement {
5555
className: "remove-button"
5656
},
5757
},
58-
onItemAdd: function() {
59-
if (!originalSelect.multiple || !this.isOpen) return;
60-
61-
this.setTextboxValue("");
62-
this.refreshOptions();
63-
},
64-
onLoad: function() {
65-
originalSelect.tomselect.setValue(
66-
originalSelect.getAttribute("data-selected")?.split(",") || [],
67-
true
68-
);
69-
},
7058
onType: function() {
7159
if (!originalSelect.multiple && !this.currentResults.items.length) {
7260
this.setTextboxValue("");
@@ -76,47 +64,82 @@ class SolidusSelect extends HTMLSelectElement {
7664
};
7765

7866
if (originalSelect.getAttribute("data-src")) {
79-
settings.load = originalSelect.loadOnce.bind(originalSelect);
67+
settings.plugins.virtual_scroll = true
68+
settings.firstUrl = () => originalSelect.getAttribute("data-src");
69+
settings.load = originalSelect.loadOptions.bind(originalSelect);
70+
settings.shouldLoad = (query) => query.length > 1
8071
settings.preload = true;
8172
settings.valueField = originalSelect.getAttribute("data-option-value-field") || "id";
8273
settings.labelField = originalSelect.getAttribute("data-option-label-field") || "name";
8374
settings.searchField = [settings.labelField];
8475
settings.render = {
8576
loading: function() {
8677
return "<div class='loading'>Loading</div>";
78+
},
79+
loading_more: function() {
80+
return "<div class='loading-more disabled'>Loading...</div>";
8781
}
88-
}
82+
};
8983
}
9084

9185
return settings;
9286
}
9387

94-
// Fetch all options from remote source and remove #load callback
95-
// https://tom-select.js.org/examples/remote/
96-
async loadOnce(query, callback) {
97-
// Avoid queueing more load requests (e.g. searching while options are still loading) if there's one already running
98-
if (this.tomselect.loading > 1) {
99-
callback();
100-
return;
88+
buildUrl(query) {
89+
const url = new URL(this.tomselect.getUrl(query));
90+
if (!query) return url;
91+
92+
url.searchParams.set(this.getAttribute("data-query-param"), query);
93+
return url.toString();
94+
}
95+
96+
// Fetch all options from remote source and setup pagination if needed
97+
async loadOptions(query, callback) {
98+
const { options, next } = await this.fetchOptions(query);
99+
if (next) {
100+
this.tomselect.setNextUrl(query, next);
101101
}
102102

103-
const options = await this.fetchOptions();
104103
callback(options);
105-
this.tomselect.settings.load = null;
106104
}
107105

108106
// Fetch options from remote source. If options data is nested in json response, specify path to it with "data-json-path"
109107
// E.g. https://whatcms.org/API/List data is deep nested in json response: `{ result: { list: [...] } }`, so
110108
// in order to access it, specify attributes as follows:
111109
// "data-src"="https://whatcms.org/API/List"
112110
// "data-json-path"="result.list"
113-
async fetchOptions() {
111+
async fetchOptions(query) {
114112
const dataPath = this.getAttribute("data-json-path");
115-
const response = await fetch(this.getAttribute("data-src"));
113+
const response = await fetch(this.buildUrl(query), { headers: { "Accept": "application/json" } });
114+
const next = parseLinkHeader(response.headers.get("Link")).next;
116115
const json = await response.json();
117-
if (!dataPath) return json;
118116

119-
return dataPath.split('.').reduce((acc, key) => acc && acc[key], json);
117+
let options;
118+
if (!dataPath) {
119+
options = json;
120+
} else {
121+
options = dataPath.split('.').reduce((acc, key) => acc && acc[key], json);
122+
}
123+
124+
return { options, next };
125+
}
126+
127+
fixDropdownScroll() {
128+
// https://github.com/orchidjs/tom-select/issues/556
129+
// https://github.com/orchidjs/tom-select/issues/867
130+
this.patch("onOptionSelect");
131+
this.patch("loadCallback");
132+
}
133+
134+
patch(fnName) {
135+
const originalFn = this.tomselect[fnName];
136+
this.tomselect.hook("instead", fnName, function() {
137+
const originalScrollToOption = this.scrollToOption;
138+
139+
this.scrollToOption = () => {};
140+
originalFn.apply(this, arguments);
141+
this.scrollToOption = originalScrollToOption;
142+
});
120143
}
121144
}
122145

0 commit comments

Comments
 (0)