@radioactive-labs/plutonium 0.50.0 → 0.51.0
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/package.json +10 -10
- package/src/css/slim_select.css +4 -0
- package/src/dist/css/plutonium.css +1 -11
- package/src/dist/js/plutonium.js +1009 -1214
- package/src/dist/js/plutonium.js.map +3 -3
- package/src/dist/js/plutonium.min.js +52 -51
- package/src/dist/js/plutonium.min.js.map +3 -3
- package/src/js/controllers/slim_select_controller.js +61 -0
- package/src/js/turbo/turbo_actions.js +33 -0
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="slim-select"
|
|
4
|
+
//
|
|
5
|
+
// Optional values (used by the typeahead-capable ResourceSelect):
|
|
6
|
+
// typeahead-url — backend endpoint that returns
|
|
7
|
+
// {results: [{value, label}, ...], has_more: bool}.
|
|
8
|
+
// When present, SlimSelect's built-in client-side
|
|
9
|
+
// filter is replaced by a debounced fetch through
|
|
10
|
+
// this URL.
|
|
4
11
|
export default class extends Controller {
|
|
12
|
+
static values = {
|
|
13
|
+
typeaheadUrl: String,
|
|
14
|
+
typeaheadDebounceMs: { type: Number, default: 200 }
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
connect() {
|
|
6
18
|
if (this.slimSelect) return;
|
|
7
19
|
|
|
@@ -47,9 +59,18 @@ export default class extends Controller {
|
|
|
47
59
|
settings.openPosition = "auto";
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
const events = {};
|
|
63
|
+
|
|
64
|
+
if (this.hasTypeaheadUrlValue && this.typeaheadUrlValue) {
|
|
65
|
+
// Replace SlimSelect's client-side filter with a server fetch.
|
|
66
|
+
// Returns the SlimSelect data array shape: {value, text}.
|
|
67
|
+
events.search = (search, currentData) => this.#typeaheadFetch(search, currentData);
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
this.slimSelect = new SlimSelect({
|
|
51
71
|
select: this.element,
|
|
52
72
|
settings: settings,
|
|
73
|
+
events: events,
|
|
53
74
|
});
|
|
54
75
|
|
|
55
76
|
// Add event listeners for better positioning
|
|
@@ -177,6 +198,46 @@ export default class extends Controller {
|
|
|
177
198
|
this.#cleanupSlimSelect();
|
|
178
199
|
}
|
|
179
200
|
|
|
201
|
+
// Server-driven search. SlimSelect calls events.search on each
|
|
202
|
+
// keystroke; we debounce so that rapid typing produces a single
|
|
203
|
+
// request, and abort any in-flight fetch when a newer one starts.
|
|
204
|
+
// Returns a Promise resolving to either a DataArray (rendered as
|
|
205
|
+
// options) or a string (rendered as the no-results label).
|
|
206
|
+
#typeaheadFetch(search, _currentData) {
|
|
207
|
+
if (this._typeaheadDebounce) clearTimeout(this._typeaheadDebounce);
|
|
208
|
+
if (this._typeaheadAbort) this._typeaheadAbort.abort();
|
|
209
|
+
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
this._typeaheadDebounce = setTimeout(() => {
|
|
212
|
+
this._typeaheadAbort = new AbortController();
|
|
213
|
+
this.#performTypeaheadFetch(search, this._typeaheadAbort.signal).then(resolve);
|
|
214
|
+
}, this.typeaheadDebounceMsValue);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async #performTypeaheadFetch(search, signal) {
|
|
219
|
+
const url = new URL(this.typeaheadUrlValue, window.location.origin);
|
|
220
|
+
url.searchParams.set("q", search || "");
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetch(url.toString(), {
|
|
224
|
+
headers: { Accept: "application/json" },
|
|
225
|
+
signal: signal,
|
|
226
|
+
});
|
|
227
|
+
if (!res.ok) return "Search failed";
|
|
228
|
+
const json = await res.json();
|
|
229
|
+
const results = Array.isArray(json.results) ? json.results : [];
|
|
230
|
+
return results.map((row) => ({
|
|
231
|
+
value: String(row.value ?? ""),
|
|
232
|
+
text: String(row.label ?? ""),
|
|
233
|
+
}));
|
|
234
|
+
} catch (e) {
|
|
235
|
+
if (e.name === "AbortError") return [];
|
|
236
|
+
console.warn("[slim-select] typeahead error", e);
|
|
237
|
+
return "Search failed";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
#handleMorph() {
|
|
181
242
|
if (!this.element.isConnected) return;
|
|
182
243
|
|
|
@@ -6,3 +6,36 @@ Turbo.StreamActions.redirect = function () {
|
|
|
6
6
|
const url = this.getAttribute("url")
|
|
7
7
|
Turbo.visit(url)
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
// Closes the <dialog> rendered inside the targeted turbo-frame and
|
|
11
|
+
// empties the frame so the dialog can be re-opened later. Used by the
|
|
12
|
+
// stacked-modal flow: after a successful create inside the secondary
|
|
13
|
+
// modal, the server tells the browser to dismiss it.
|
|
14
|
+
Turbo.StreamActions.close_frame = function () {
|
|
15
|
+
const frameId = this.getAttribute("target")
|
|
16
|
+
if (!frameId) return
|
|
17
|
+
|
|
18
|
+
const frame = document.getElementById(frameId)
|
|
19
|
+
if (!frame) return
|
|
20
|
+
|
|
21
|
+
const dialog = frame.querySelector("dialog")
|
|
22
|
+
if (dialog && typeof dialog.close === "function") dialog.close()
|
|
23
|
+
|
|
24
|
+
// Clearing the frame's content keeps a future visit to the same URL
|
|
25
|
+
// re-fetching (turbo would otherwise treat the frame as cached).
|
|
26
|
+
frame.innerHTML = ""
|
|
27
|
+
frame.removeAttribute("src")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Reloads the targeted turbo-frame from its current src. Used after a
|
|
31
|
+
// secondary-modal action mutates data the primary modal depends on
|
|
32
|
+
// (e.g. a newly created association option) so the primary re-renders.
|
|
33
|
+
Turbo.StreamActions.reload_frame = function () {
|
|
34
|
+
const frameId = this.getAttribute("target")
|
|
35
|
+
if (!frameId) return
|
|
36
|
+
|
|
37
|
+
const frame = document.getElementById(frameId)
|
|
38
|
+
if (!frame || typeof frame.reload !== "function") return
|
|
39
|
+
|
|
40
|
+
frame.reload()
|
|
41
|
+
}
|