katalyst-koi 4.18.0 → 4.19.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feb798fc8f8baf7c9b7e78269e46c510d04c5e345ed7f605490fab898f2428ec
4
- data.tar.gz: 7be3b6e4c9de2969f6118283004bef6d511cddf2fd2277886fdafbdd54177700
3
+ metadata.gz: ade3236f89d6ce47ecd5c365e7f47897f3b520983168a674ce14681ccadb805b
4
+ data.tar.gz: 54cfce0d885a190456ef78f72d7db89d8c9078e5cead1a62ae7c8d9459523f58
5
5
  SHA512:
6
- metadata.gz: '088d74784a4b12c90295467d4d5d3ef0baea13e66bc34cce1f1237ebc8ced0696711d77cd4333b5d9245edeaeaaa1c67236027facaa9ab37642ece2dfdd33245'
7
- data.tar.gz: 348b7140826626abc87d0f421c15c43a8d2e36dc7fc792b5756dcb2f877412af2204f49042d6ef416b9dda8e65399377d8ecfb5f6127dce3480c935665f4d59e
6
+ metadata.gz: 0d7d676ab5aaada95ad72039a4942e4768e1a8b3dc958557768ae2e9a2250a67ddc4c9b0e10561e441b8e5ed415ca808f5967638eafcd953a385f94064f5481c
7
+ data.tar.gz: 1fbf60e25099b5ae5fc9c1794b85347d7983b412eb304432fd597dd003699e98751c9114e994659e3c7f570b49c2a01315c6c544e6dcf06367244d3a5c461fd4
@@ -0,0 +1,518 @@
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
+
10
+ const application = Application.start();
11
+
12
+ class ClipboardController extends Controller {
13
+ static targets = ["source"];
14
+
15
+ static classes = ["supported"];
16
+
17
+ connect() {
18
+ if ("clipboard" in navigator) {
19
+ this.element.classList.add(this.supportedClass);
20
+ }
21
+ }
22
+
23
+ copy(event) {
24
+ event.preventDefault();
25
+ navigator.clipboard.writeText(this.sourceTarget.value);
26
+
27
+ this.element.classList.add("copied");
28
+ setTimeout(() => {
29
+ this.element.classList.remove("copied");
30
+ }, 2000);
31
+ }
32
+ }
33
+
34
+ class FlashController extends Controller {
35
+ close(e) {
36
+ e.target.closest("li").remove();
37
+
38
+ // remove the flash container if there are no more flashes
39
+ if (this.element.children.length === 0) {
40
+ this.element.remove();
41
+ }
42
+ }
43
+ }
44
+
45
+ class KeyboardController extends Controller {
46
+ static values = {
47
+ mapping: String,
48
+ depth: { type: Number, default: 2 },
49
+ };
50
+
51
+ event(cause) {
52
+ if (isFormField(cause.target) || this.#ignore(cause)) return;
53
+
54
+ const key = this.describeEvent(cause);
55
+
56
+ this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);
57
+
58
+ // test whether the tail of the buffer matches any of the configured chords
59
+ const action = this.buffer.reduceRight((mapping, key) => {
60
+ if (typeof mapping === "string" || typeof mapping === "undefined") {
61
+ return mapping;
62
+ } else {
63
+ return mapping[key];
64
+ }
65
+ }, this.mappings);
66
+
67
+ // if we don't have a string we may have a miss or an incomplete chord
68
+ if (typeof action !== "string") return;
69
+
70
+ // clear the buffer and prevent the key from being consumed elsewhere
71
+ this.buffer = [];
72
+ cause.preventDefault();
73
+
74
+ // fire the configured event
75
+ const event = new CustomEvent(action, {
76
+ detail: { cause: cause },
77
+ bubbles: true,
78
+ });
79
+ cause.target.dispatchEvent(event);
80
+ }
81
+
82
+ /**
83
+ * @param event KeyboardEvent input event to describe
84
+ * @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)
85
+ */
86
+ describeEvent(event) {
87
+ return [
88
+ event.ctrlKey && "C",
89
+ event.metaKey && "M",
90
+ event.altKey && "A",
91
+ event.shiftKey && "S",
92
+ event.code,
93
+ ]
94
+ .filter((w) => w)
95
+ .join("-");
96
+ }
97
+
98
+ /**
99
+ * Build a tree for efficiently looking up key chords, where the last key in the sequence
100
+ * is the first key in tree.
101
+ */
102
+ get mappings() {
103
+ const inputs = this.mappingValue
104
+ .replaceAll(/\s+/g, " ")
105
+ .split(" ")
106
+ .filter((f) => f.length > 0);
107
+ const mappings = {};
108
+
109
+ inputs.forEach((mapping) => this.#parse(mappings, mapping));
110
+
111
+ // memoize the result
112
+ Object.defineProperty(this, "mappings", {
113
+ value: mappings,
114
+ writable: false,
115
+ });
116
+
117
+ return mappings;
118
+ }
119
+
120
+ /**
121
+ * Parse a key chord pattern and an event and store it in the inverted tree lookup structure.
122
+ *
123
+ * @param mappings inverted tree lookup for key chords
124
+ * @param mapping input definition, e.g. "C-KeyC+C-KeyV->paste"
125
+ */
126
+ #parse(mappings, mapping) {
127
+ const [pattern, event] = mapping.split("->");
128
+ const keys = pattern.split("+");
129
+ const first = keys.shift();
130
+
131
+ mappings = keys.reduceRight(
132
+ (mappings, key) => (mappings[key] ||= {}),
133
+ mappings,
134
+ );
135
+ mappings[first] = event;
136
+ }
137
+
138
+ /**
139
+ * Ignore modifier keys, as they will be captured in normal key presses.
140
+ *
141
+ * @param event KeyboardEvent
142
+ * @returns {boolean} true if key event should be ignored
143
+ */
144
+ #ignore(event) {
145
+ switch (event.code) {
146
+ case "ControlLeft":
147
+ case "ControlRight":
148
+ case "MetaLeft":
149
+ case "MetaRight":
150
+ case "ShiftLeft":
151
+ case "ShiftRight":
152
+ case "AltLeft":
153
+ case "AltRight":
154
+ return true;
155
+ default:
156
+ return false;
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Detect input nodes where we should not listen for events.
163
+ *
164
+ * Credit: github.com
165
+ */
166
+ function isFormField(element) {
167
+ if (!(element instanceof HTMLElement)) {
168
+ return false;
169
+ }
170
+
171
+ const name = element.nodeName.toLowerCase();
172
+ const type = (element.getAttribute("type") || "").toLowerCase();
173
+ return (
174
+ name === "select" ||
175
+ name === "textarea" ||
176
+ name === "trix-editor" ||
177
+ (name === "input" &&
178
+ type !== "submit" &&
179
+ type !== "reset" &&
180
+ type !== "checkbox" &&
181
+ type !== "radio" &&
182
+ type !== "file") ||
183
+ element.isContentEditable
184
+ );
185
+ }
186
+
187
+ class ModalController extends Controller {
188
+ static targets = ["dialog"];
189
+
190
+ connect() {
191
+ this.element.addEventListener("turbo:submit-end", this.onSubmit);
192
+ }
193
+
194
+ disconnect() {
195
+ this.element.removeEventListener("turbo:submit-end", this.onSubmit);
196
+ }
197
+
198
+ outside(e) {
199
+ if (e.target.tagName === "DIALOG") this.dismiss();
200
+ }
201
+
202
+ dismiss() {
203
+ if (!this.dialogTarget) return;
204
+ if (!this.dialogTarget.open) this.dialogTarget.close();
205
+
206
+ this.element.removeAttribute("src");
207
+ this.dialogTarget.remove();
208
+ }
209
+
210
+ dialogTargetConnected(dialog) {
211
+ dialog.showModal();
212
+ }
213
+
214
+ onSubmit = (event) => {
215
+ if (
216
+ event.detail.success &&
217
+ "closeDialog" in event.detail.formSubmission?.submitter?.dataset
218
+ ) {
219
+ this.dialogTarget.close();
220
+ this.element.removeAttribute("src");
221
+ this.dialogTarget.remove();
222
+ }
223
+ };
224
+ }
225
+
226
+ class NavigationController extends Controller {
227
+ static targets = ["filter"];
228
+
229
+ filter() {
230
+ const filter = this.filterTarget.value;
231
+ this.clearFilter(filter);
232
+
233
+ if (filter.length > 0) {
234
+ this.applyFilter(filter);
235
+ }
236
+ }
237
+
238
+ go() {
239
+ this.element.querySelector("li:not([hidden]) > a").click();
240
+ }
241
+
242
+ clear() {
243
+ if (this.filterTarget.value.length === 0) this.filterTarget.blur();
244
+ }
245
+
246
+ applyFilter(filter) {
247
+ // hide items that don't match the search filter
248
+ this.links
249
+ .filter(
250
+ (li) =>
251
+ !this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),
252
+ )
253
+ .forEach((li) => {
254
+ li.toggleAttribute("hidden", true);
255
+ });
256
+
257
+ this.menus
258
+ .filter((li) => !li.matches("li:has(li:not([hidden]) > a)"))
259
+ .forEach((li) => {
260
+ li.toggleAttribute("hidden", true);
261
+ });
262
+ }
263
+
264
+ clearFilter(filter) {
265
+ this.element.querySelectorAll("li").forEach((li) => {
266
+ li.toggleAttribute("hidden", false);
267
+ });
268
+ }
269
+
270
+ prefixSearch(needle, haystack) {
271
+ const haystackLength = haystack.length;
272
+ const needleLength = needle.length;
273
+ if (needleLength > haystackLength) {
274
+ return false;
275
+ }
276
+ if (needleLength === haystackLength) {
277
+ return needle === haystack;
278
+ }
279
+ outer: for (let i = 0, j = 0; i < needleLength; i++) {
280
+ const needleChar = needle.charCodeAt(i);
281
+ if (needleChar === 32) {
282
+ // skip ahead to next space in the haystack
283
+ while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
284
+ continue;
285
+ }
286
+ while (j < haystackLength) {
287
+ if (haystack.charCodeAt(j++) === needleChar) continue outer;
288
+ // skip ahead to the next space in the haystack
289
+ while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
290
+ }
291
+ return false;
292
+ }
293
+ return true;
294
+ }
295
+
296
+ toggle() {
297
+ this.element.open ? this.close() : this.open();
298
+ }
299
+
300
+ open() {
301
+ if (!this.element.open) this.element.showModal();
302
+ }
303
+
304
+ close() {
305
+ if (this.element.open) this.element.close();
306
+ }
307
+
308
+ click(e) {
309
+ if (e.target === this.element) this.close();
310
+ }
311
+
312
+ onMorphAttribute = (e) => {
313
+ if (e.target !== this.element) return;
314
+
315
+ switch (e.detail.attributeName) {
316
+ case "open":
317
+ e.preventDefault();
318
+ }
319
+ };
320
+
321
+ get links() {
322
+ return Array.from(this.element.querySelectorAll("li:has(> a)"));
323
+ }
324
+
325
+ get menus() {
326
+ return Array.from(this.element.querySelectorAll("li:has(> ul)"));
327
+ }
328
+ }
329
+
330
+ class NavigationToggleController extends Controller {
331
+ trigger() {
332
+ this.dispatch("toggle", { prefix: "navigation", bubbles: true });
333
+ }
334
+ }
335
+
336
+ class PagyNavController extends Controller {
337
+ connect() {
338
+ document.addEventListener("shortcut:page-prev", this.prevPage);
339
+ document.addEventListener("shortcut:page-next", this.nextPage);
340
+ }
341
+
342
+ disconnect() {
343
+ document.removeEventListener("shortcut:page-prev", this.prevPage);
344
+ document.removeEventListener("shortcut:page-next", this.nextPage);
345
+ }
346
+
347
+ nextPage = () => {
348
+ this.element.querySelector("a:last-child").click();
349
+ };
350
+
351
+ prevPage = () => {
352
+ this.element.querySelector("a:first-child").click();
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Connect an input (e.g. title) to slug.
358
+ */
359
+ class SluggableController extends Controller {
360
+ static targets = ["source", "slug"];
361
+ static values = {
362
+ slug: String,
363
+ };
364
+
365
+ sourceChanged(e) {
366
+ if (this.slugValue === "") {
367
+ this.slugTarget.value = parameterize(this.sourceTarget.value);
368
+ }
369
+ }
370
+
371
+ slugChanged(e) {
372
+ this.slugValue = this.slugTarget.value;
373
+ }
374
+ }
375
+
376
+ function parameterize(input) {
377
+ return input
378
+ .toLowerCase()
379
+ .replace(/'/g, "-")
380
+ .replace(/[^-\w\s]/g, "")
381
+ .replace(/[^a-z0-9]+/g, "-")
382
+ .replace(/(^-|-$)/g, "");
383
+ }
384
+
385
+ class WebauthnAuthenticationController extends Controller {
386
+ static targets = ["response"];
387
+ static values = {
388
+ options: Object,
389
+ };
390
+
391
+ async authenticate() {
392
+ const credential = await navigator.credentials.get(this.options);
393
+
394
+ this.responseTarget.value = JSON.stringify(credential.toJSON());
395
+
396
+ this.element.requestSubmit();
397
+ }
398
+
399
+ get options() {
400
+ return {
401
+ publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
402
+ this.optionsValue,
403
+ ),
404
+ };
405
+ }
406
+ }
407
+
408
+ class WebauthnRegistrationController extends Controller {
409
+ static targets = ["response"];
410
+ static values = {
411
+ options: Object,
412
+ };
413
+
414
+ submit(e) {
415
+ if (this.responseTarget.value) return;
416
+
417
+ e.preventDefault();
418
+ this.createCredential().then(() => {
419
+ e.target.submit();
420
+ });
421
+ }
422
+
423
+ async createCredential() {
424
+ const credential = await navigator.credentials.create(this.options);
425
+ this.responseTarget.value = JSON.stringify(credential.toJSON());
426
+ }
427
+
428
+ get options() {
429
+ return {
430
+ publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(
431
+ this.optionsValue,
432
+ ),
433
+ };
434
+ }
435
+ }
436
+
437
+ application.load(content);
438
+ application.load(govuk);
439
+ application.load(navigation);
440
+ application.load(tables);
441
+
442
+ const Definitions = [
443
+ {
444
+ identifier: "clipboard",
445
+ controllerConstructor: ClipboardController,
446
+ },
447
+ {
448
+ identifier: "flash",
449
+ controllerConstructor: FlashController,
450
+ },
451
+ {
452
+ identifier: "keyboard",
453
+ controllerConstructor: KeyboardController,
454
+ },
455
+ {
456
+ identifier: "modal",
457
+ controllerConstructor: ModalController,
458
+ },
459
+ {
460
+ identifier: "navigation",
461
+ controllerConstructor: NavigationController,
462
+ },
463
+ {
464
+ identifier: "navigation-toggle",
465
+ controllerConstructor: NavigationToggleController,
466
+ },
467
+ {
468
+ identifier: "pagy-nav",
469
+ controllerConstructor: PagyNavController,
470
+ },
471
+ {
472
+ identifier: "sluggable",
473
+ controllerConstructor: SluggableController,
474
+ },
475
+ {
476
+ identifier: "webauthn-authentication",
477
+ controllerConstructor: WebauthnAuthenticationController,
478
+ },
479
+ {
480
+ identifier: "webauthn-registration",
481
+ controllerConstructor: WebauthnRegistrationController,
482
+ },
483
+ ];
484
+
485
+ // dynamically attempt to load hw_combobox_controller, this is an optional dependency
486
+ await import('controllers/hw_combobox_controller')
487
+ .then(({ default: HwComboboxController }) => {
488
+ Definitions.push({
489
+ identifier: "hw-combobox",
490
+ controllerConstructor: HwComboboxController,
491
+ });
492
+ })
493
+ .catch(() => null);
494
+
495
+ application.load(Definitions);
496
+
497
+ class KoiToolbarElement extends HTMLElement {
498
+ constructor() {
499
+ super();
500
+
501
+ this.setAttribute("role", "toolbar");
502
+ }
503
+ }
504
+
505
+ customElements.define("koi-toolbar", KoiToolbarElement);
506
+
507
+ /** Initialize GOVUK */
508
+ function initGOVUK() {
509
+ document.body.classList.toggle("js-enabled", true);
510
+ document.body.classList.toggle(
511
+ "govuk-frontend-supported",
512
+ "noModule" in HTMLScriptElement.prototype,
513
+ );
514
+ initAll();
515
+ }
516
+
517
+ window.addEventListener("turbo:load", initGOVUK);
518
+ if (window.Turbo) initGOVUK();