katalyst-koi 5.0.0.alpha.2 → 5.0.0.beta.1

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/katalyst/koi.esm.js +832 -0
  3. data/app/assets/builds/katalyst/koi.js +832 -0
  4. data/app/assets/builds/katalyst/koi.min.js +2 -0
  5. data/app/assets/builds/katalyst/koi.min.js.map +1 -0
  6. data/app/assets/config/koi.js +0 -12
  7. data/app/assets/stylesheets/koi/blocks/modal.css +4 -1
  8. data/app/assets/stylesheets/koi/blocks/tables/query.css +5 -4
  9. data/app/assets/stylesheets/koi/blocks/tables/table.css +21 -20
  10. data/app/assets/stylesheets/koi/forms/combobox.css +81 -0
  11. data/app/assets/stylesheets/koi/forms/index.css +3 -0
  12. data/app/assets/stylesheets/koi/forms/input.css +8 -7
  13. data/app/assets/stylesheets/koi/forms/textarea.css +6 -0
  14. data/app/assets/stylesheets/koi/forms/trix.css +6 -2
  15. data/app/components/koi/pagy_nav_component.rb +7 -0
  16. data/app/controllers/concerns/koi/controller/has_attachments.rb +1 -1
  17. data/app/controllers/concerns/koi/controller.rb +2 -0
  18. data/app/helpers/koi/modal_helper.rb +15 -13
  19. data/app/{assets/javascripts/koi/admin.js → javascript/koi/application.js} +2 -2
  20. data/app/{assets/javascripts → javascript}/koi/controllers/form_request_submit_controller.js +1 -1
  21. data/app/javascript/koi/controllers/index.js +89 -0
  22. data/app/{assets/javascripts/koi/controllers/koi → javascript/koi/controllers}/modal_controller.js +1 -1
  23. data/app/{assets/javascripts → javascript}/koi/controllers/show_hide_controller.js +1 -1
  24. data/app/javascript/koi/elements/index.js +1 -0
  25. data/app/models/admin/user.rb +2 -2
  26. data/app/models/concerns/koi/model/archivable.rb +3 -3
  27. data/app/views/layouts/koi/application.html.erb +1 -1
  28. data/config/importmap.rb +1 -3
  29. data/lib/generators/koi/helpers/attribute_helpers.rb +11 -8
  30. data/lib/generators/koi/helpers/resource_helpers.rb +9 -1
  31. data/lib/koi/config.rb +2 -0
  32. data/lib/koi/engine.rb +10 -7
  33. data/lib/koi/form_builder.rb +0 -1
  34. data/lib/koi.rb +0 -7
  35. metadata +27 -37
  36. data/app/assets/javascripts/koi/controllers/document_field_controller.js +0 -26
  37. data/app/assets/javascripts/koi/controllers/file_field_controller.js +0 -152
  38. data/app/assets/javascripts/koi/controllers/image_field_controller.js +0 -24
  39. data/app/assets/javascripts/koi/controllers/index.js +0 -15
  40. data/app/assets/javascripts/koi/elements/index.js +0 -1
  41. data/app/mailers/koi/application_mailer.rb +0 -8
  42. data/app/views/katalyst/content/asides/_aside.html+form.erb +0 -6
  43. data/app/views/katalyst/content/columns/_column.html+form.erb +0 -5
  44. data/app/views/katalyst/content/contents/_content.html+form.erb +0 -8
  45. data/app/views/katalyst/content/figures/_figure.html+form.erb +0 -9
  46. data/app/views/katalyst/content/groups/_group.html+form.erb +0 -5
  47. data/app/views/katalyst/content/items/_item.html+form.erb +0 -5
  48. data/app/views/katalyst/content/sections/_section.html+form.erb +0 -5
  49. data/app/views/katalyst/content/tables/_table.html+form.erb +0 -36
  50. data/lib/koi/form/elements/document.rb +0 -47
  51. data/lib/koi/form/elements/file_element.rb +0 -128
  52. data/lib/koi/form/elements/image.rb +0 -44
  53. data/lib/koi/form/govuk_extensions.rb +0 -73
  54. /data/app/{assets/javascripts → javascript}/koi/controllers/application.js +0 -0
  55. /data/app/{assets/javascripts → javascript}/koi/controllers/clipboard_controller.js +0 -0
  56. /data/app/{assets/javascripts → javascript}/koi/controllers/flash_controller.js +0 -0
  57. /data/app/{assets/javascripts → javascript}/koi/controllers/index_actions_controller.js +0 -0
  58. /data/app/{assets/javascripts → javascript}/koi/controllers/keyboard_controller.js +0 -0
  59. /data/app/{assets/javascripts → javascript}/koi/controllers/navigation_controller.js +0 -0
  60. /data/app/{assets/javascripts → javascript}/koi/controllers/navigation_toggle_controller.js +0 -0
  61. /data/app/{assets/javascripts → javascript}/koi/controllers/pagy_nav_controller.js +0 -0
  62. /data/app/{assets/javascripts → javascript}/koi/controllers/sluggable_controller.js +0 -0
  63. /data/app/{assets/javascripts → javascript}/koi/controllers/webauthn_authentication_controller.js +0 -0
  64. /data/app/{assets/javascripts → javascript}/koi/controllers/webauthn_registration_controller.js +0 -0
  65. /data/app/{assets/javascripts → javascript}/koi/elements/toolbar.js +0 -0
  66. /data/app/{assets/javascripts → javascript}/koi/utils/transition.js +0 -0
  67. /data/lib/{katalyst/koi.rb → katalyst-koi.rb} +0 -0
@@ -0,0 +1,832 @@
1
+ import '@hotwired/turbo-rails';
2
+ import govuk, { initAll } from '@katalyst/govuk-formbuilder';
3
+ import '@rails/actiontext';
4
+ import 'trix';
5
+ import { Application, Controller } from '@hotwired/stimulus';
6
+ import content from '@katalyst/content';
7
+ import navigation from '@katalyst/navigation';
8
+ import tables from '@katalyst/tables';
9
+ import { get, parseRequestOptionsFromJSON, create, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
10
+ import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading';
11
+
12
+ const application = Application.start();
13
+
14
+ class ClipboardController extends Controller {
15
+ static targets = ["source"];
16
+
17
+ static classes = ["supported"];
18
+
19
+ connect() {
20
+ if ("clipboard" in navigator) {
21
+ this.element.classList.add(this.supportedClass);
22
+ }
23
+ }
24
+
25
+ copy(event) {
26
+ event.preventDefault();
27
+ navigator.clipboard.writeText(this.sourceTarget.value);
28
+
29
+ this.element.classList.add("copied");
30
+ setTimeout(() => {
31
+ this.element.classList.remove("copied");
32
+ }, 2000);
33
+ }
34
+ }
35
+
36
+ class FlashController extends Controller {
37
+ close(e) {
38
+ e.target.closest("li").remove();
39
+
40
+ // remove the flash container if there are no more flashes
41
+ if (this.element.children.length === 0) {
42
+ this.element.remove();
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ A stimulus controller to request form submissions.
49
+ This controller should be attached to a form element.
50
+ */
51
+ class FormRequestSubmitController extends Controller {
52
+ requestSubmit() {
53
+ this.element.requestSubmit();
54
+ }
55
+ }
56
+
57
+ class IndexActionsController extends Controller {
58
+ static targets = ["create", "search", "sort"];
59
+
60
+ initialize() {
61
+ // debounce search
62
+ this.update = debounce(this, this.update);
63
+ }
64
+
65
+ disconnect() {
66
+ clearTimeout(this.timer);
67
+ }
68
+
69
+ create() {
70
+ this.createTarget.click();
71
+ }
72
+
73
+ search() {
74
+ this.searchTarget.focus();
75
+ }
76
+
77
+ clear() {
78
+ this.searchTarget.value = "";
79
+ this.searchTarget.closest("form").requestSubmit();
80
+ }
81
+
82
+ update() {
83
+ this.searchTarget.closest("form").requestSubmit();
84
+ }
85
+
86
+ submit() {
87
+ const shouldFocus = document.activeElement === this.searchTarget;
88
+
89
+ if (this.searchTarget.value === "") {
90
+ this.searchTarget.disabled = true;
91
+ }
92
+ if (this.sortTarget.value === "") {
93
+ this.sortTarget.disabled = true;
94
+ }
95
+
96
+ // restore state and focus after submit
97
+ Promise.resolve().then(() => {
98
+ this.searchTarget.disabled = false;
99
+ this.sortTarget.disabled = false;
100
+ if (shouldFocus) {
101
+ this.searchTarget.focus();
102
+ }
103
+ });
104
+ }
105
+ }
106
+
107
+ function debounce(self, f) {
108
+ return (...args) => {
109
+ clearTimeout(self.timer);
110
+ self.timer = setTimeout(() => {
111
+ f.apply(self, ...args);
112
+ }, 300);
113
+ };
114
+ }
115
+
116
+ class KeyboardController extends Controller {
117
+ static values = {
118
+ mapping: String,
119
+ depth: { type: Number, default: 2 },
120
+ };
121
+
122
+ event(cause) {
123
+ if (isFormField(cause.target) || this.#ignore(cause)) return;
124
+
125
+ const key = this.describeEvent(cause);
126
+
127
+ this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);
128
+
129
+ // test whether the tail of the buffer matches any of the configured chords
130
+ const action = this.buffer.reduceRight((mapping, key) => {
131
+ if (typeof mapping === "string" || typeof mapping === "undefined") {
132
+ return mapping;
133
+ } else {
134
+ return mapping[key];
135
+ }
136
+ }, this.mappings);
137
+
138
+ // if we don't have a string we may have a miss or an incomplete chord
139
+ if (typeof action !== "string") return;
140
+
141
+ // clear the buffer and prevent the key from being consumed elsewhere
142
+ this.buffer = [];
143
+ cause.preventDefault();
144
+
145
+ // fire the configured event
146
+ const event = new CustomEvent(action, {
147
+ detail: { cause: cause },
148
+ bubbles: true,
149
+ });
150
+ cause.target.dispatchEvent(event);
151
+ }
152
+
153
+ /**
154
+ * @param event KeyboardEvent input event to describe
155
+ * @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)
156
+ */
157
+ describeEvent(event) {
158
+ return [
159
+ event.ctrlKey && "C",
160
+ event.metaKey && "M",
161
+ event.altKey && "A",
162
+ event.shiftKey && "S",
163
+ event.code,
164
+ ]
165
+ .filter((w) => w)
166
+ .join("-");
167
+ }
168
+
169
+ /**
170
+ * Build a tree for efficiently looking up key chords, where the last key in the sequence
171
+ * is the first key in tree.
172
+ */
173
+ get mappings() {
174
+ const inputs = this.mappingValue
175
+ .replaceAll(/\s+/g, " ")
176
+ .split(" ")
177
+ .filter((f) => f.length > 0);
178
+ const mappings = {};
179
+
180
+ inputs.forEach((mapping) => this.#parse(mappings, mapping));
181
+
182
+ // memoize the result
183
+ Object.defineProperty(this, "mappings", {
184
+ value: mappings,
185
+ writable: false,
186
+ });
187
+
188
+ return mappings;
189
+ }
190
+
191
+ /**
192
+ * Parse a key chord pattern and an event and store it in the inverted tree lookup structure.
193
+ *
194
+ * @param mappings inverted tree lookup for key chords
195
+ * @param mapping input definition, e.g. "C-KeyC+C-KeyV->paste"
196
+ */
197
+ #parse(mappings, mapping) {
198
+ const [pattern, event] = mapping.split("->");
199
+ const keys = pattern.split("+");
200
+ const first = keys.shift();
201
+
202
+ mappings = keys.reduceRight(
203
+ (mappings, key) => (mappings[key] ||= {}),
204
+ mappings,
205
+ );
206
+ mappings[first] = event;
207
+ }
208
+
209
+ /**
210
+ * Ignore modifier keys, as they will be captured in normal key presses.
211
+ *
212
+ * @param event KeyboardEvent
213
+ * @returns {boolean} true if key event should be ignored
214
+ */
215
+ #ignore(event) {
216
+ switch (event.code) {
217
+ case "ControlLeft":
218
+ case "ControlRight":
219
+ case "MetaLeft":
220
+ case "MetaRight":
221
+ case "ShiftLeft":
222
+ case "ShiftRight":
223
+ case "AltLeft":
224
+ case "AltRight":
225
+ return true;
226
+ default:
227
+ return false;
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Detect input nodes where we should not listen for events.
234
+ *
235
+ * Credit: github.com
236
+ */
237
+ function isFormField(element) {
238
+ if (!(element instanceof HTMLElement)) {
239
+ return false;
240
+ }
241
+
242
+ const name = element.nodeName.toLowerCase();
243
+ const type = (element.getAttribute("type") || "").toLowerCase();
244
+ return (
245
+ name === "select" ||
246
+ name === "textarea" ||
247
+ name === "trix-editor" ||
248
+ (name === "input" &&
249
+ type !== "submit" &&
250
+ type !== "reset" &&
251
+ type !== "checkbox" &&
252
+ type !== "radio" &&
253
+ type !== "file") ||
254
+ element.isContentEditable
255
+ );
256
+ }
257
+
258
+ class ModalController extends Controller {
259
+ static targets = ["dialog"];
260
+
261
+ connect() {
262
+ this.element.addEventListener("turbo:submit-end", this.onSubmit);
263
+ }
264
+
265
+ disconnect() {
266
+ this.element.removeEventListener("turbo:submit-end", this.onSubmit);
267
+ }
268
+
269
+ outside(e) {
270
+ if (e.target.tagName === "DIALOG") this.dismiss();
271
+ }
272
+
273
+ dismiss() {
274
+ if (!this.dialogTarget) return;
275
+ if (!this.dialogTarget.open) this.dialogTarget.close();
276
+
277
+ this.element.removeAttribute("src");
278
+ this.dialogTarget.remove();
279
+ }
280
+
281
+ dialogTargetConnected(dialog) {
282
+ dialog.showModal();
283
+ }
284
+
285
+ onSubmit = (event) => {
286
+ if (
287
+ event.detail.success &&
288
+ "closeDialog" in event.detail.formSubmission?.submitter?.dataset
289
+ ) {
290
+ this.dialogTarget.close();
291
+ this.element.removeAttribute("src");
292
+ this.dialogTarget.remove();
293
+ }
294
+ };
295
+ }
296
+
297
+ class NavigationController extends Controller {
298
+ static targets = ["filter"];
299
+
300
+ filter() {
301
+ const filter = this.filterTarget.value;
302
+ this.clearFilter(filter);
303
+
304
+ if (filter.length > 0) {
305
+ this.applyFilter(filter);
306
+ }
307
+ }
308
+
309
+ go() {
310
+ this.element.querySelector("li:not([hidden]) > a").click();
311
+ }
312
+
313
+ clear() {
314
+ if (this.filterTarget.value.length === 0) this.filterTarget.blur();
315
+ }
316
+
317
+ applyFilter(filter) {
318
+ // hide items that don't match the search filter
319
+ this.links
320
+ .filter(
321
+ (li) =>
322
+ !this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),
323
+ )
324
+ .forEach((li) => {
325
+ li.toggleAttribute("hidden", true);
326
+ });
327
+
328
+ this.menus
329
+ .filter((li) => !li.matches("li:has(li:not([hidden]) > a)"))
330
+ .forEach((li) => {
331
+ li.toggleAttribute("hidden", true);
332
+ });
333
+ }
334
+
335
+ clearFilter(filter) {
336
+ this.element.querySelectorAll("li").forEach((li) => {
337
+ li.toggleAttribute("hidden", false);
338
+ });
339
+ }
340
+
341
+ prefixSearch(needle, haystack) {
342
+ const haystackLength = haystack.length;
343
+ const needleLength = needle.length;
344
+ if (needleLength > haystackLength) {
345
+ return false;
346
+ }
347
+ if (needleLength === haystackLength) {
348
+ return needle === haystack;
349
+ }
350
+ outer: for (let i = 0, j = 0; i < needleLength; i++) {
351
+ const needleChar = needle.charCodeAt(i);
352
+ if (needleChar === 32) {
353
+ // skip ahead to next space in the haystack
354
+ while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
355
+ continue;
356
+ }
357
+ while (j < haystackLength) {
358
+ if (haystack.charCodeAt(j++) === needleChar) continue outer;
359
+ // skip ahead to the next space in the haystack
360
+ while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
361
+ }
362
+ return false;
363
+ }
364
+ return true;
365
+ }
366
+
367
+ toggle() {
368
+ this.element.open ? this.close() : this.open();
369
+ }
370
+
371
+ open() {
372
+ if (!this.element.open) this.element.showModal();
373
+ }
374
+
375
+ close() {
376
+ if (this.element.open) this.element.close();
377
+ }
378
+
379
+ click(e) {
380
+ if (e.target === this.element) this.close();
381
+ }
382
+
383
+ onMorphAttribute = (e) => {
384
+ if (e.target !== this.element) return;
385
+
386
+ switch (e.detail.attributeName) {
387
+ case "open":
388
+ e.preventDefault();
389
+ }
390
+ };
391
+
392
+ get links() {
393
+ return Array.from(this.element.querySelectorAll("li:has(> a)"));
394
+ }
395
+
396
+ get menus() {
397
+ return Array.from(this.element.querySelectorAll("li:has(> ul)"));
398
+ }
399
+ }
400
+
401
+ class NavigationToggleController extends Controller {
402
+ trigger() {
403
+ this.dispatch("toggle", { prefix: "navigation", bubbles: true });
404
+ }
405
+ }
406
+
407
+ class PagyNavController extends Controller {
408
+ connect() {
409
+ document.addEventListener("shortcut:page-prev", this.prevPage);
410
+ document.addEventListener("shortcut:page-next", this.nextPage);
411
+ }
412
+
413
+ disconnect() {
414
+ document.removeEventListener("shortcut:page-prev", this.prevPage);
415
+ document.removeEventListener("shortcut:page-next", this.nextPage);
416
+ }
417
+
418
+ nextPage = () => {
419
+ this.element.querySelector("a:last-child").click();
420
+ };
421
+
422
+ prevPage = () => {
423
+ this.element.querySelector("a:first-child").click();
424
+ };
425
+ }
426
+
427
+ const DEFAULT_DELAY = 250;
428
+
429
+ /**
430
+ * A utility class for managing CSS transition animations.
431
+ *
432
+ * Transition uses Javascript timers to track state instead of relying on
433
+ * CSS transition events, which is a more complicated API. Please call `cancel`
434
+ * when the node being animated is detached from the DOM to avoid unexpected
435
+ * errors or animation glitches.
436
+ *
437
+ * Transition assumes that CSS already specifies styles to achieve the expected
438
+ * start and end states. Transition adds temporary overrides and then animates
439
+ * between those values using CSS transitions. For example, to use the collapse
440
+ * transition:
441
+ *
442
+ * @example
443
+ * // CSS:
444
+ * target {
445
+ * max-height: unset;
446
+ * overflow: 0;
447
+ * }
448
+ * target.hidden {
449
+ * max-height: 0;
450
+ * }
451
+ *
452
+ * @example
453
+ * // Javascript
454
+ * target.addClass("hidden");
455
+ * new Transition(target).collapse().start();
456
+ */
457
+ class Transition {
458
+ constructor(target, options) {
459
+ const { delay } = this._setDefaults(options);
460
+
461
+ this.target = target;
462
+ this.runner = new Runner(this, delay);
463
+ this.properties = [];
464
+
465
+ this.startingCallbacks = [];
466
+ this.startedCallbacks = [];
467
+ this.completeCallbacks = [];
468
+ }
469
+
470
+ add(property) {
471
+ this.properties.push(property);
472
+ return this;
473
+ }
474
+
475
+ /** Adds callback for transition events */
476
+ addCallback(type, callback) {
477
+ switch (type) {
478
+ case "starting":
479
+ this.startingCallbacks.push(callback);
480
+ break;
481
+ case "started":
482
+ this.startedCallbacks.push(callback);
483
+ break;
484
+ case "complete":
485
+ this.completeCallbacks.push(callback);
486
+ break;
487
+ }
488
+ return this;
489
+ }
490
+
491
+ /** Collapse an element in place, assumes overflow is set appropriately, margin is not collapsed */
492
+ collapse() {
493
+ return this.add(
494
+ new PropertyTransition(
495
+ "max-height",
496
+ `${this.target.scrollHeight}px`,
497
+ "0px",
498
+ ),
499
+ );
500
+ }
501
+
502
+ /** Restore a collapsed element */
503
+ expand() {
504
+ return this.add(
505
+ new PropertyTransition(
506
+ "max-height",
507
+ "0px",
508
+ `${this.target.scrollHeight}px`,
509
+ ),
510
+ );
511
+ }
512
+
513
+ /** Slide an element left or right by its scroll width, assumes position relative */
514
+ slideOut(direction) {
515
+ return this.add(
516
+ new PropertyTransition(direction, "0px", `-${this.target.scrollWidth}px`),
517
+ );
518
+ }
519
+
520
+ /** Restore an element that has been slid */
521
+ slideIn(direction) {
522
+ return this.add(
523
+ new PropertyTransition(direction, `-${this.target.scrollWidth}px`, "0px"),
524
+ );
525
+ }
526
+
527
+ /** Cause an element to become transparent by transforming opacity */
528
+ fadeOut() {
529
+ return this.add(new PropertyTransition("opacity", "100%", "0%"));
530
+ }
531
+
532
+ /** Cause a transparent element to become visible again */
533
+ fadeIn() {
534
+ return this.add(new PropertyTransition("opacity", "0%", "100%"));
535
+ }
536
+
537
+ start(callback = null) {
538
+ // start the runner on next tick so that any side-effects of the current execution can occur first
539
+ requestAnimationFrame(() => {
540
+ this.runner.start(this.target);
541
+ if (callback) callback();
542
+ });
543
+ return this;
544
+ }
545
+
546
+ cancel() {
547
+ this.runner.stop(this.target);
548
+ return this;
549
+ }
550
+
551
+ _starting() {
552
+ const event = new Event("transition:starting");
553
+ this.startingCallbacks.forEach((cb) => cb(event));
554
+ this.target.dispatchEvent(event);
555
+ }
556
+
557
+ _started() {
558
+ const event = new Event("transition:started");
559
+ this.startedCallbacks.forEach((cb) => cb(event));
560
+ this.target.dispatchEvent(event);
561
+ }
562
+
563
+ _complete() {
564
+ const event = new Event("transition:complete");
565
+ this.completeCallbacks.forEach((cb) => cb(event));
566
+ this.target.dispatchEvent(event);
567
+ }
568
+
569
+ _setDefaults(options) {
570
+ return Object.assign({ delay: DEFAULT_DELAY }, options);
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Encapsulates internal execution and timing functionality for `Transition`
576
+ */
577
+ class Runner {
578
+ constructor(transition, delay) {
579
+ this.transition = transition;
580
+ this.running = null;
581
+ this.delay = delay;
582
+ }
583
+
584
+ start(target) {
585
+ // 1. Set the initial state(s)
586
+ this.transition.properties.forEach((t) => t.onStarting(target));
587
+
588
+ // 2. On next update, set transition and final state(s)
589
+ requestAnimationFrame(() => this.onStarted(target));
590
+
591
+ // 3. After transition has finished, clean up
592
+ this.running = setTimeout(() => this.stop(target, true), this.delay);
593
+
594
+ this.transition._starting();
595
+ }
596
+
597
+ onStarted(target) {
598
+ target.style.transitionProperty = this.transition.properties
599
+ .map((t) => t.property)
600
+ .join(",");
601
+ target.style.transitionDuration = `${this.delay}ms`;
602
+ this.transition.properties.forEach((t) => t.onStarted(target));
603
+
604
+ this.transition._started();
605
+ }
606
+
607
+ stop(target, timeout = false) {
608
+ if (!this.running) return;
609
+ if (!timeout) clearTimeout(this.running);
610
+
611
+ this.running = null;
612
+
613
+ target.style.removeProperty("transition-property");
614
+ target.style.removeProperty("transition-duration");
615
+ this.transition.properties.forEach((t) => t.onComplete(target));
616
+
617
+ this.transition._complete();
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Represents animation of a single CSS property. Currently only CSS animations
623
+ * are supported, but this could be a natural extension point for Javascript
624
+ * animations in the future.
625
+ */
626
+ class PropertyTransition {
627
+ constructor(property, from, to) {
628
+ this.property = property;
629
+ this.from = from;
630
+ this.to = to;
631
+ }
632
+
633
+ onStarting(target) {
634
+ target.style.setProperty(this.property, this.from);
635
+ }
636
+
637
+ onStarted(target) {
638
+ target.style.setProperty(this.property, this.to);
639
+ }
640
+
641
+ onComplete(target) {
642
+ target.style.removeProperty(this.property);
643
+ }
644
+ }
645
+
646
+ class ShowHideController extends Controller {
647
+ static targets = ["content"];
648
+
649
+ toggle() {
650
+ const element = this.contentTarget;
651
+ const hide = element.toggleAttribute("data-collapsed");
652
+
653
+ // cancel previous animation, if any
654
+ if (this.transition) this.transition.cancel();
655
+
656
+ const transition = (this.transition = new Transition(element)
657
+ .addCallback("starting", function () {
658
+ element.setAttribute("data-collapsed-transitioning", "true");
659
+ })
660
+ .addCallback("complete", function () {
661
+ element.removeAttribute("data-collapsed-transitioning");
662
+ }));
663
+ hide ? transition.collapse() : transition.expand();
664
+
665
+ transition.start();
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Connect an input (e.g. title) to slug.
671
+ */
672
+ class SluggableController extends Controller {
673
+ static targets = ["source", "slug"];
674
+ static values = {
675
+ slug: String,
676
+ };
677
+
678
+ sourceChanged(e) {
679
+ if (this.slugValue === "") {
680
+ this.slugTarget.value = parameterize(this.sourceTarget.value);
681
+ }
682
+ }
683
+
684
+ slugChanged(e) {
685
+ this.slugValue = this.slugTarget.value;
686
+ }
687
+ }
688
+
689
+ function parameterize(input) {
690
+ return input
691
+ .toLowerCase()
692
+ .replace(/'/g, "-")
693
+ .replace(/[^-\w\s]/g, "")
694
+ .replace(/[^a-z0-9]+/g, "-")
695
+ .replace(/(^-|-$)/g, "");
696
+ }
697
+
698
+ class WebauthnAuthenticationController extends Controller {
699
+ static targets = ["response"];
700
+ static values = { options: Object };
701
+
702
+ authenticate() {
703
+ get(this.options).then((response) => {
704
+ this.responseTarget.value = JSON.stringify(response);
705
+
706
+ this.element.requestSubmit();
707
+ });
708
+ }
709
+
710
+ get options() {
711
+ return parseRequestOptionsFromJSON(this.optionsValue);
712
+ }
713
+ }
714
+
715
+ class WebauthnRegistrationController extends Controller {
716
+ static values = {
717
+ options: Object,
718
+ response: String,
719
+ };
720
+ static targets = ["intro", "nickname", "response"];
721
+
722
+ submit(e) {
723
+ if (
724
+ this.responseTarget.value === "" &&
725
+ e.submitter.formMethod !== "dialog"
726
+ ) {
727
+ e.preventDefault();
728
+ this.createCredential();
729
+ }
730
+ }
731
+
732
+ async createCredential() {
733
+ const response = await create(this.options);
734
+
735
+ this.responseValue = JSON.stringify(response);
736
+ this.responseTarget.value = JSON.stringify(response);
737
+ }
738
+
739
+ responseValueChanged(response) {
740
+ const responsePresent = response !== "";
741
+ this.introTarget.toggleAttribute("hidden", responsePresent);
742
+ this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
743
+ }
744
+
745
+ get options() {
746
+ return parseCreationOptionsFromJSON(this.optionsValue);
747
+ }
748
+ }
749
+
750
+ application.load(content);
751
+ application.load(govuk);
752
+ application.load(navigation);
753
+ application.load(tables);
754
+
755
+ const Definitions = [
756
+ {
757
+ identifier: "clipboard",
758
+ controllerConstructor: ClipboardController,
759
+ },
760
+ {
761
+ identifier: "flash",
762
+ controllerConstructor: FlashController,
763
+ },
764
+ {
765
+ identifier: "form-request-submit",
766
+ controllerConstructor: FormRequestSubmitController,
767
+ },
768
+ {
769
+ identifier: "index-actions",
770
+ controllerConstructor: IndexActionsController,
771
+ },
772
+ {
773
+ identifier: "keyboard",
774
+ controllerConstructor: KeyboardController,
775
+ },
776
+ {
777
+ identifier: "modal",
778
+ controllerConstructor: ModalController,
779
+ },
780
+ {
781
+ identifier: "navigation",
782
+ controllerConstructor: NavigationController,
783
+ },
784
+ {
785
+ identifier: "navigation-toggle",
786
+ controllerConstructor: NavigationToggleController,
787
+ },
788
+ {
789
+ identifier: "pagy-nav",
790
+ controllerConstructor: PagyNavController,
791
+ },
792
+ {
793
+ identifier: "show-hide",
794
+ controllerConstructor: ShowHideController,
795
+ },
796
+ {
797
+ identifier: "sluggable",
798
+ controllerConstructor: SluggableController,
799
+ },
800
+ {
801
+ identifier: "webauthn-authentication",
802
+ controllerConstructor: WebauthnAuthenticationController,
803
+ },
804
+ {
805
+ identifier: "webauthn-registration",
806
+ controllerConstructor: WebauthnRegistrationController,
807
+ },
808
+ ];
809
+
810
+ application.load(Definitions);
811
+
812
+ eagerLoadControllersFrom("admin/controllers", application);
813
+
814
+ class KoiToolbarElement extends HTMLElement {
815
+ constructor() {
816
+ super();
817
+
818
+ this.setAttribute("role", "toolbar");
819
+ }
820
+ }
821
+
822
+ customElements.define("koi-toolbar", KoiToolbarElement);
823
+
824
+ /** Let GOVUK know that we've got JS enabled */
825
+ window.addEventListener("turbo:load", () => {
826
+ document.body.classList.toggle("js-enabled", true);
827
+ document.body.classList.toggle(
828
+ "govuk-frontend-supported",
829
+ "noModule" in HTMLScriptElement.prototype,
830
+ );
831
+ initAll();
832
+ });