katalyst-koi 4.19.0 → 4.20.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: ade3236f89d6ce47ecd5c365e7f47897f3b520983168a674ce14681ccadb805b
4
- data.tar.gz: 54cfce0d885a190456ef78f72d7db89d8c9078e5cead1a62ae7c8d9459523f58
3
+ metadata.gz: 3deb30461e50ff4888407793a4996b2d8399d75165aae7960f32a6d1675f1a62
4
+ data.tar.gz: ed8227a14a6e3cf241ac9fbdc214948a99af5abd394b741b71f177dd401c9699
5
5
  SHA512:
6
- metadata.gz: 0d7d676ab5aaada95ad72039a4942e4768e1a8b3dc958557768ae2e9a2250a67ddc4c9b0e10561e441b8e5ed415ca808f5967638eafcd953a385f94064f5481c
7
- data.tar.gz: 1fbf60e25099b5ae5fc9c1794b85347d7983b412eb304432fd597dd003699e98751c9114e994659e3c7f570b49c2a01315c6c544e6dcf06367244d3a5c461fd4
6
+ metadata.gz: 7625ea35ee89d5aeb0f0a76e7d4a54f2de970d5ae7b70b95664f5daab487c151d600e106394b3f6bf2e8e0b9abd6c9a477a0da09aabfe6aad872c20fde269714
7
+ data.tar.gz: e524219d772ad8fa57ee969ed440834f17e7e059d25b97e4383d34f8de35d6c9b7ba8a89d2ce0dad0b46ebc96d1559dca41cee0aacc640835f124aa062ea1080
@@ -35,9 +35,7 @@ module Admin
35
35
  end
36
36
 
37
37
  def destroy
38
- record_sign_out!(current_admin_user)
39
-
40
- session[:admin_user_id] = nil
38
+ destroy_admin_session!(current_admin_user)
41
39
 
42
40
  redirect_to new_admin_session_path
43
41
  end
@@ -95,9 +93,7 @@ module Admin
95
93
  end
96
94
 
97
95
  def admin_sign_in(admin_user)
98
- record_sign_in!(admin_user)
99
-
100
- session[:admin_user_id] = admin_user.id
96
+ create_admin_session!(admin_user)
101
97
 
102
98
  redirect_to(url_from(params[:redirect].presence) || admin_dashboard_path, status: :see_other)
103
99
  end
@@ -22,9 +22,7 @@ module Admin
22
22
 
23
23
  def update
24
24
  if (@admin_user = Admin::User.find_by_token_for(:password_reset, params[:token]))
25
- record_sign_in!(admin_user)
26
-
27
- session[:admin_user_id] = admin_user.id
25
+ create_admin_session!(admin_user)
28
26
 
29
27
  redirect_to admin_admin_user_path(admin_user), status: :see_other, notice: t("koi.auth.token_consumed")
30
28
  else
@@ -49,8 +49,12 @@ module Koi
49
49
  def authenticate_local_admin
50
50
  return if admin_signed_in? || !Rails.env.development?
51
51
 
52
- session[:admin_user_id] =
53
- Admin::User.where(email: %W[#{ENV.fetch('USER', nil)}@katalyst.com.au admin@katalyst.com.au]).first&.id
52
+ admin_user = Admin::User.where(email: %W[#{ENV.fetch('USER', nil)}@katalyst.com.au admin@katalyst.com.au]).first
53
+
54
+ return unless admin_user
55
+
56
+ session[:admin_user_id] = admin_user.id
57
+ session[:admin_user_signed_in_at] = Time.current.iso8601(6)
54
58
 
55
59
  flash.delete(:redirect) if (redirect = flash[:redirect])
56
60
 
@@ -3,33 +3,46 @@
3
3
  module Koi
4
4
  module Controller
5
5
  module RecordsAuthentication
6
- def update_last_sign_in(admin_user)
7
- return if admin_user.current_sign_in_at.blank?
6
+ def create_admin_session!(admin_user)
7
+ sign_in_at = Time.current
8
8
 
9
- admin_user.last_sign_in_at = admin_user.current_sign_in_at
10
- admin_user.last_sign_in_ip = admin_user.current_sign_in_ip
11
- end
12
-
13
- def record_sign_in!(admin_user)
14
9
  update_last_sign_in(admin_user)
15
10
 
16
- admin_user.current_sign_in_at = Time.current
11
+ admin_user.current_sign_in_at = sign_in_at
17
12
  admin_user.current_sign_in_ip = request.remote_ip
18
13
  admin_user.sign_in_count += 1
19
14
 
20
15
  admin_user.save!
16
+
17
+ session[:admin_user_id] = admin_user.id
18
+ session[:admin_user_signed_in_at] = sign_in_at.iso8601(6)
21
19
  end
22
20
 
23
- def record_sign_out!(admin_user)
21
+ def destroy_admin_session!(admin_user)
22
+ session[:admin_user_id] = nil
23
+ session[:admin_user_signed_in_at] = nil
24
+
24
25
  return unless admin_user
25
26
 
27
+ sign_out_at = Time.current
28
+
26
29
  update_last_sign_in(admin_user)
27
30
 
31
+ admin_user.last_sign_out_at = sign_out_at
28
32
  admin_user.current_sign_in_at = nil
29
33
  admin_user.current_sign_in_ip = nil
30
34
 
31
35
  admin_user.save!
32
36
  end
37
+
38
+ private
39
+
40
+ def update_last_sign_in(admin_user)
41
+ return if admin_user.current_sign_in_at.blank?
42
+
43
+ admin_user.last_sign_in_at = admin_user.current_sign_in_at
44
+ admin_user.last_sign_in_ip = admin_user.current_sign_in_ip
45
+ end
33
46
  end
34
47
  end
35
48
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddLastSignOutAtToAdminUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :admins, :last_sign_out_at, :datetime
6
+ end
7
+ end
@@ -18,8 +18,9 @@ module Koi
18
18
  def admin_call(env)
19
19
  request = ActionDispatch::Request.new(env)
20
20
  session = ActionDispatch::Request::Session.find(request)
21
+ authenticated = authenticated?(session)
21
22
 
22
- if requires_authentication?(request) && !authenticated?(session)
23
+ if requires_authentication?(request) && !authenticated
23
24
  # Set the redirection path for returning the user to their requested path after login
24
25
  if request.get?
25
26
  request.flash[:redirect] = request.fullpath
@@ -39,7 +40,34 @@ module Koi
39
40
  end
40
41
 
41
42
  def authenticated?(session)
42
- session[:admin_user_id].present?
43
+ admin_user = Admin::User.find_by(id: session[:admin_user_id])
44
+ unless admin_user
45
+ clear_admin_session(session)
46
+ return false
47
+ end
48
+
49
+ signed_in_at = session_signed_in_at(session)
50
+ if signed_in_at.blank? || session_expired?(admin_user, signed_in_at)
51
+ clear_admin_session(session)
52
+ return false
53
+ end
54
+
55
+ true
56
+ end
57
+
58
+ def session_signed_in_at(session)
59
+ Time.zone.parse(session[:admin_user_signed_in_at].to_s)
60
+ rescue ArgumentError
61
+ nil
62
+ end
63
+
64
+ def session_expired?(admin_user, signed_in_at)
65
+ admin_user.last_sign_out_at.present? && signed_in_at < admin_user.last_sign_out_at
66
+ end
67
+
68
+ def clear_admin_session(session)
69
+ session.delete(:admin_user_id)
70
+ session.delete(:admin_user_signed_in_at)
43
71
  end
44
72
  end
45
73
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-koi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.19.0
4
+ version: 4.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
@@ -242,10 +242,6 @@ files:
242
242
  - MIT-LICENSE
243
243
  - README.md
244
244
  - Upgrade.md
245
- - app/assets/builds/katalyst/koi.esm.js
246
- - app/assets/builds/katalyst/koi.js
247
- - app/assets/builds/katalyst/koi.min.js
248
- - app/assets/builds/katalyst/koi.min.js.map
249
245
  - app/assets/builds/koi/admin.css
250
246
  - app/assets/config/koi.js
251
247
  - app/assets/images/koi/application/chevron-right.svg
@@ -478,6 +474,7 @@ files:
478
474
  - db/migrate/20231211005214_add_status_code_to_url_rewrites.rb
479
475
  - db/migrate/20241214060913_add_otp_secret_to_admin_users.rb
480
476
  - db/migrate/20250204060748_create_well_knowns.rb
477
+ - db/migrate/20260501000000_add_last_sign_out_at_to_admin_users.rb
481
478
  - db/seeds.rb
482
479
  - lib/generators/koi/active_record/active_record_generator.rb
483
480
  - lib/generators/koi/admin/USAGE
@@ -1,518 +0,0 @@
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();
@@ -1,518 +0,0 @@
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();
@@ -1,2 +0,0 @@
1
- import"@hotwired/turbo-rails";import e,{initAll as t}from"@katalyst/govuk-formbuilder";import"@rails/actiontext";import"trix";import{Application as i,Controller as r}from"@hotwired/stimulus";import s from"@katalyst/content";import o from"@katalyst/navigation";import n from"@katalyst/tables";const a=i.start();a.load(s),a.load(e),a.load(o),a.load(n);const l=[{identifier:"clipboard",controllerConstructor:class extends r{static targets=["source"];static classes=["supported"];connect(){"clipboard"in navigator&&this.element.classList.add(this.supportedClass)}copy(e){e.preventDefault(),navigator.clipboard.writeText(this.sourceTarget.value),this.element.classList.add("copied"),setTimeout(()=>{this.element.classList.remove("copied")},2e3)}}},{identifier:"flash",controllerConstructor:class extends r{close(e){e.target.closest("li").remove(),0===this.element.children.length&&this.element.remove()}}},{identifier:"keyboard",controllerConstructor:class extends r{static values={mapping:String,depth:{type:Number,default:2}};event(e){if(function(e){if(!(e instanceof HTMLElement))return!1;const t=e.nodeName.toLowerCase(),i=(e.getAttribute("type")||"").toLowerCase();return"select"===t||"textarea"===t||"trix-editor"===t||"input"===t&&"submit"!==i&&"reset"!==i&&"checkbox"!==i&&"radio"!==i&&"file"!==i||e.isContentEditable}(e.target)||this.#e(e))return;const t=this.describeEvent(e);this.buffer=[...this.buffer||[],t].slice(0-this.depthValue);const i=this.buffer.reduceRight((e,t)=>"string"==typeof e||void 0===e?e:e[t],this.mappings);if("string"!=typeof i)return;this.buffer=[],e.preventDefault();const r=new CustomEvent(i,{detail:{cause:e},bubbles:!0});e.target.dispatchEvent(r)}describeEvent(e){return[e.ctrlKey&&"C",e.metaKey&&"M",e.altKey&&"A",e.shiftKey&&"S",e.code].filter(e=>e).join("-")}get mappings(){const e=this.mappingValue.replaceAll(/\s+/g," ").split(" ").filter(e=>e.length>0),t={};return e.forEach(e=>this.#t(t,e)),Object.defineProperty(this,"mappings",{value:t,writable:!1}),t}#t(e,t){const[i,r]=t.split("->"),s=i.split("+"),o=s.shift();(e=s.reduceRight((e,t)=>e[t]||={},e))[o]=r}#e(e){switch(e.code){case"ControlLeft":case"ControlRight":case"MetaLeft":case"MetaRight":case"ShiftLeft":case"ShiftRight":case"AltLeft":case"AltRight":return!0;default:return!1}}}},{identifier:"modal",controllerConstructor:class extends r{static targets=["dialog"];connect(){this.element.addEventListener("turbo:submit-end",this.onSubmit)}disconnect(){this.element.removeEventListener("turbo:submit-end",this.onSubmit)}outside(e){"DIALOG"===e.target.tagName&&this.dismiss()}dismiss(){this.dialogTarget&&(this.dialogTarget.open||this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}dialogTargetConnected(e){e.showModal()}onSubmit=e=>{e.detail.success&&"closeDialog"in e.detail.formSubmission?.submitter?.dataset&&(this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}}},{identifier:"navigation",controllerConstructor:class extends r{static targets=["filter"];filter(){const e=this.filterTarget.value;this.clearFilter(e),e.length>0&&this.applyFilter(e)}go(){this.element.querySelector("li:not([hidden]) > a").click()}clear(){0===this.filterTarget.value.length&&this.filterTarget.blur()}applyFilter(e){this.links.filter(t=>!this.prefixSearch(e.toLowerCase(),t.innerText.toLowerCase())).forEach(e=>{e.toggleAttribute("hidden",!0)}),this.menus.filter(e=>!e.matches("li:has(li:not([hidden]) > a)")).forEach(e=>{e.toggleAttribute("hidden",!0)})}clearFilter(e){this.element.querySelectorAll("li").forEach(e=>{e.toggleAttribute("hidden",!1)})}prefixSearch(e,t){const i=t.length,r=e.length;if(r>i)return!1;if(r===i)return e===t;e:for(let s=0,o=0;s<r;s++){const r=e.charCodeAt(s);if(32!==r){for(;o<i;){if(t.charCodeAt(o++)===r)continue e;for(;o<i&&32!==t.charCodeAt(o++););}return!1}for(;o<i&&32!==t.charCodeAt(o++););}return!0}toggle(){this.element.open?this.close():this.open()}open(){this.element.open||this.element.showModal()}close(){this.element.open&&this.element.close()}click(e){e.target===this.element&&this.close()}onMorphAttribute=e=>{if(e.target===this.element&&"open"===e.detail.attributeName)e.preventDefault()};get links(){return Array.from(this.element.querySelectorAll("li:has(> a)"))}get menus(){return Array.from(this.element.querySelectorAll("li:has(> ul)"))}}},{identifier:"navigation-toggle",controllerConstructor:class extends r{trigger(){this.dispatch("toggle",{prefix:"navigation",bubbles:!0})}}},{identifier:"pagy-nav",controllerConstructor:class extends r{connect(){document.addEventListener("shortcut:page-prev",this.prevPage),document.addEventListener("shortcut:page-next",this.nextPage)}disconnect(){document.removeEventListener("shortcut:page-prev",this.prevPage),document.removeEventListener("shortcut:page-next",this.nextPage)}nextPage=()=>{this.element.querySelector("a:last-child").click()};prevPage=()=>{this.element.querySelector("a:first-child").click()}}},{identifier:"sluggable",controllerConstructor:class extends r{static targets=["source","slug"];static values={slug:String};sourceChanged(e){""===this.slugValue&&(this.slugTarget.value=this.sourceTarget.value.toLowerCase().replace(/'/g,"-").replace(/[^-\w\s]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,""))}slugChanged(e){this.slugValue=this.slugTarget.value}}},{identifier:"webauthn-authentication",controllerConstructor:class extends r{static targets=["response"];static values={options:Object};async authenticate(){const e=await navigator.credentials.get(this.options);this.responseTarget.value=JSON.stringify(e.toJSON()),this.element.requestSubmit()}get options(){return{publicKey:PublicKeyCredential.parseRequestOptionsFromJSON(this.optionsValue)}}}},{identifier:"webauthn-registration",controllerConstructor:class extends r{static targets=["response"];static values={options:Object};submit(e){this.responseTarget.value||(e.preventDefault(),this.createCredential().then(()=>{e.target.submit()}))}async createCredential(){const e=await navigator.credentials.create(this.options);this.responseTarget.value=JSON.stringify(e.toJSON())}get options(){return{publicKey:PublicKeyCredential.parseCreationOptionsFromJSON(this.optionsValue)}}}}];await import("controllers/hw_combobox_controller").then(({default:e})=>{l.push({identifier:"hw-combobox",controllerConstructor:e})}).catch(()=>null),a.load(l);class c extends HTMLElement{constructor(){super(),this.setAttribute("role","toolbar")}}function u(){document.body.classList.toggle("js-enabled",!0),document.body.classList.toggle("govuk-frontend-supported","noModule"in HTMLScriptElement.prototype),t()}customElements.define("koi-toolbar",c),window.addEventListener("turbo:load",u),window.Turbo&&u();
2
- //# sourceMappingURL=koi.min.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"koi.min.js","sources":["../../../javascript/koi/controllers/application.js","../../../javascript/koi/controllers/index.js","../../../javascript/koi/controllers/clipboard_controller.js","../../../javascript/koi/controllers/flash_controller.js","../../../javascript/koi/controllers/keyboard_controller.js","../../../javascript/koi/controllers/modal_controller.js","../../../javascript/koi/controllers/navigation_controller.js","../../../javascript/koi/controllers/navigation_toggle_controller.js","../../../javascript/koi/controllers/pagy_nav_controller.js","../../../javascript/koi/controllers/sluggable_controller.js","../../../javascript/koi/controllers/webauthn_authentication_controller.js","../../../javascript/koi/controllers/webauthn_registration_controller.js","../../../javascript/koi/elements/toolbar.js","../../../javascript/koi/application.js"],"sourcesContent":["import { Application } from \"@hotwired/stimulus\";\n\nconst application = Application.start();\n\nexport { application };\n","import { application } from \"./application\";\n\nimport content from \"@katalyst/content\";\napplication.load(content);\n\nimport govuk from \"@katalyst/govuk-formbuilder\";\napplication.load(govuk);\n\nimport navigation from \"@katalyst/navigation\";\napplication.load(navigation);\n\nimport tables from \"@katalyst/tables\";\napplication.load(tables);\n\nimport ClipboardController from \"./clipboard_controller\";\nimport FlashController from \"./flash_controller\";\nimport KeyboardController from \"./keyboard_controller\";\nimport ModalController from \"./modal_controller\";\nimport NavigationController from \"./navigation_controller\";\nimport NavigationToggleController from \"./navigation_toggle_controller\";\nimport PagyNavController from \"./pagy_nav_controller\";\nimport SluggableController from \"./sluggable_controller\";\nimport WebauthnAuthenticationController from \"./webauthn_authentication_controller\";\nimport WebauthnRegistrationController from \"./webauthn_registration_controller\";\n\nconst Definitions = [\n {\n identifier: \"clipboard\",\n controllerConstructor: ClipboardController,\n },\n {\n identifier: \"flash\",\n controllerConstructor: FlashController,\n },\n {\n identifier: \"keyboard\",\n controllerConstructor: KeyboardController,\n },\n {\n identifier: \"modal\",\n controllerConstructor: ModalController,\n },\n {\n identifier: \"navigation\",\n controllerConstructor: NavigationController,\n },\n {\n identifier: \"navigation-toggle\",\n controllerConstructor: NavigationToggleController,\n },\n {\n identifier: \"pagy-nav\",\n controllerConstructor: PagyNavController,\n },\n {\n identifier: \"sluggable\",\n controllerConstructor: SluggableController,\n },\n {\n identifier: \"webauthn-authentication\",\n controllerConstructor: WebauthnAuthenticationController,\n },\n {\n identifier: \"webauthn-registration\",\n controllerConstructor: WebauthnRegistrationController,\n },\n];\n\n// dynamically attempt to load hw_combobox_controller, this is an optional dependency\nawait import(\"controllers/hw_combobox_controller\")\n .then(({ default: HwComboboxController }) => {\n Definitions.push({\n identifier: \"hw-combobox\",\n controllerConstructor: HwComboboxController,\n });\n })\n .catch(() => null);\n\napplication.load(Definitions);\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ClipboardController extends Controller {\n static targets = [\"source\"];\n\n static classes = [\"supported\"];\n\n connect() {\n if (\"clipboard\" in navigator) {\n this.element.classList.add(this.supportedClass);\n }\n }\n\n copy(event) {\n event.preventDefault();\n navigator.clipboard.writeText(this.sourceTarget.value);\n\n this.element.classList.add(\"copied\");\n setTimeout(() => {\n this.element.classList.remove(\"copied\");\n }, 2000);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class FlashController extends Controller {\n close(e) {\n e.target.closest(\"li\").remove();\n\n // remove the flash container if there are no more flashes\n if (this.element.children.length === 0) {\n this.element.remove();\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nconst DEBUG = false;\n\nexport default class KeyboardController extends Controller {\n static values = {\n mapping: String,\n depth: { type: Number, default: 2 },\n };\n\n event(cause) {\n if (isFormField(cause.target) || this.#ignore(cause)) return;\n\n const key = this.describeEvent(cause);\n\n this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);\n\n if (DEBUG) console.debug(\"[keyboard] buffer:\", ...this.buffer);\n\n // test whether the tail of the buffer matches any of the configured chords\n const action = this.buffer.reduceRight((mapping, key) => {\n if (typeof mapping === \"string\" || typeof mapping === \"undefined\") {\n return mapping;\n } else {\n return mapping[key];\n }\n }, this.mappings);\n\n // if we don't have a string we may have a miss or an incomplete chord\n if (typeof action !== \"string\") return;\n\n // clear the buffer and prevent the key from being consumed elsewhere\n this.buffer = [];\n cause.preventDefault();\n\n if (DEBUG) console.debug(\"[keyboard] event: %s\", action);\n\n // fire the configured event\n const event = new CustomEvent(action, {\n detail: { cause: cause },\n bubbles: true,\n });\n cause.target.dispatchEvent(event);\n }\n\n /**\n * @param event KeyboardEvent input event to describe\n * @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)\n */\n describeEvent(event) {\n return [\n event.ctrlKey && \"C\",\n event.metaKey && \"M\",\n event.altKey && \"A\",\n event.shiftKey && \"S\",\n event.code,\n ]\n .filter((w) => w)\n .join(\"-\");\n }\n\n /**\n * Build a tree for efficiently looking up key chords, where the last key in the sequence\n * is the first key in tree.\n */\n get mappings() {\n const inputs = this.mappingValue\n .replaceAll(/\\s+/g, \" \")\n .split(\" \")\n .filter((f) => f.length > 0);\n const mappings = {};\n\n inputs.forEach((mapping) => this.#parse(mappings, mapping));\n\n // memoize the result\n Object.defineProperty(this, \"mappings\", {\n value: mappings,\n writable: false,\n });\n\n return mappings;\n }\n\n /**\n * Parse a key chord pattern and an event and store it in the inverted tree lookup structure.\n *\n * @param mappings inverted tree lookup for key chords\n * @param mapping input definition, e.g. \"C-KeyC+C-KeyV->paste\"\n */\n #parse(mappings, mapping) {\n const [pattern, event] = mapping.split(\"->\");\n const keys = pattern.split(\"+\");\n const first = keys.shift();\n\n mappings = keys.reduceRight(\n (mappings, key) => (mappings[key] ||= {}),\n mappings,\n );\n mappings[first] = event;\n }\n\n /**\n * Ignore modifier keys, as they will be captured in normal key presses.\n *\n * @param event KeyboardEvent\n * @returns {boolean} true if key event should be ignored\n */\n #ignore(event) {\n switch (event.code) {\n case \"ControlLeft\":\n case \"ControlRight\":\n case \"MetaLeft\":\n case \"MetaRight\":\n case \"ShiftLeft\":\n case \"ShiftRight\":\n case \"AltLeft\":\n case \"AltRight\":\n return true;\n default:\n return false;\n }\n }\n}\n\n/**\n * Detect input nodes where we should not listen for events.\n *\n * Credit: github.com\n */\nfunction isFormField(element) {\n if (!(element instanceof HTMLElement)) {\n return false;\n }\n\n const name = element.nodeName.toLowerCase();\n const type = (element.getAttribute(\"type\") || \"\").toLowerCase();\n return (\n name === \"select\" ||\n name === \"textarea\" ||\n name === \"trix-editor\" ||\n (name === \"input\" &&\n type !== \"submit\" &&\n type !== \"reset\" &&\n type !== \"checkbox\" &&\n type !== \"radio\" &&\n type !== \"file\") ||\n element.isContentEditable\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ModalController extends Controller {\n static targets = [\"dialog\"];\n\n connect() {\n this.element.addEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n disconnect() {\n this.element.removeEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n outside(e) {\n if (e.target.tagName === \"DIALOG\") this.dismiss();\n }\n\n dismiss() {\n if (!this.dialogTarget) return;\n if (!this.dialogTarget.open) this.dialogTarget.close();\n\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n\n dialogTargetConnected(dialog) {\n dialog.showModal();\n }\n\n onSubmit = (event) => {\n if (\n event.detail.success &&\n \"closeDialog\" in event.detail.formSubmission?.submitter?.dataset\n ) {\n this.dialogTarget.close();\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationController extends Controller {\n static targets = [\"filter\"];\n\n filter() {\n const filter = this.filterTarget.value;\n this.clearFilter(filter);\n\n if (filter.length > 0) {\n this.applyFilter(filter);\n }\n }\n\n go() {\n this.element.querySelector(\"li:not([hidden]) > a\").click();\n }\n\n clear() {\n if (this.filterTarget.value.length === 0) this.filterTarget.blur();\n }\n\n applyFilter(filter) {\n // hide items that don't match the search filter\n this.links\n .filter(\n (li) =>\n !this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),\n )\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n\n this.menus\n .filter((li) => !li.matches(\"li:has(li:not([hidden]) > a)\"))\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n }\n\n clearFilter(filter) {\n this.element.querySelectorAll(\"li\").forEach((li) => {\n li.toggleAttribute(\"hidden\", false);\n });\n }\n\n prefixSearch(needle, haystack) {\n const haystackLength = haystack.length;\n const needleLength = needle.length;\n if (needleLength > haystackLength) {\n return false;\n }\n if (needleLength === haystackLength) {\n return needle === haystack;\n }\n outer: for (let i = 0, j = 0; i < needleLength; i++) {\n const needleChar = needle.charCodeAt(i);\n if (needleChar === 32) {\n // skip ahead to next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n continue;\n }\n while (j < haystackLength) {\n if (haystack.charCodeAt(j++) === needleChar) continue outer;\n // skip ahead to the next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n }\n return false;\n }\n return true;\n }\n\n toggle() {\n this.element.open ? this.close() : this.open();\n }\n\n open() {\n if (!this.element.open) this.element.showModal();\n }\n\n close() {\n if (this.element.open) this.element.close();\n }\n\n click(e) {\n if (e.target === this.element) this.close();\n }\n\n onMorphAttribute = (e) => {\n if (e.target !== this.element) return;\n\n switch (e.detail.attributeName) {\n case \"open\":\n e.preventDefault();\n }\n };\n\n get links() {\n return Array.from(this.element.querySelectorAll(\"li:has(> a)\"));\n }\n\n get menus() {\n return Array.from(this.element.querySelectorAll(\"li:has(> ul)\"));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationToggleController extends Controller {\n trigger() {\n this.dispatch(\"toggle\", { prefix: \"navigation\", bubbles: true });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class PagyNavController extends Controller {\n connect() {\n document.addEventListener(\"shortcut:page-prev\", this.prevPage);\n document.addEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n disconnect() {\n document.removeEventListener(\"shortcut:page-prev\", this.prevPage);\n document.removeEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n nextPage = () => {\n this.element.querySelector(\"a:last-child\").click();\n };\n\n prevPage = () => {\n this.element.querySelector(\"a:first-child\").click();\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Connect an input (e.g. title) to slug.\n */\nexport default class SluggableController extends Controller {\n static targets = [\"source\", \"slug\"];\n static values = {\n slug: String,\n };\n\n sourceChanged(e) {\n if (this.slugValue === \"\") {\n this.slugTarget.value = parameterize(this.sourceTarget.value);\n }\n }\n\n slugChanged(e) {\n this.slugValue = this.slugTarget.value;\n }\n}\n\nfunction parameterize(input) {\n return input\n .toLowerCase()\n .replace(/'/g, \"-\")\n .replace(/[^-\\w\\s]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnAuthenticationController extends Controller {\n static targets = [\"response\"];\n static values = {\n options: Object,\n };\n\n async authenticate() {\n const credential = await navigator.credentials.get(this.options);\n\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n\n this.element.requestSubmit();\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(\n this.optionsValue,\n ),\n };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnRegistrationController extends Controller {\n static targets = [\"response\"];\n static values = {\n options: Object,\n };\n\n submit(e) {\n if (this.responseTarget.value) return;\n\n e.preventDefault();\n this.createCredential().then(() => {\n e.target.submit();\n });\n }\n\n async createCredential() {\n const credential = await navigator.credentials.create(this.options);\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(\n this.optionsValue,\n ),\n };\n }\n}\n","class KoiToolbarElement extends HTMLElement {\n constructor() {\n super();\n\n this.setAttribute(\"role\", \"toolbar\");\n }\n}\n\ncustomElements.define(\"koi-toolbar\", KoiToolbarElement);\n","import \"@hotwired/turbo-rails\";\nimport { initAll } from \"@katalyst/govuk-formbuilder\";\nimport \"@rails/actiontext\";\nimport \"trix\";\n\nimport \"./controllers\";\nimport \"./elements\";\n\n/** Initialize GOVUK */\nfunction initGOVUK() {\n document.body.classList.toggle(\"js-enabled\", true);\n document.body.classList.toggle(\n \"govuk-frontend-supported\",\n \"noModule\" in HTMLScriptElement.prototype,\n );\n initAll();\n}\n\nwindow.addEventListener(\"turbo:load\", initGOVUK);\nif (window.Turbo) initGOVUK();\n"],"names":["application","Application","start","load","content","govuk","navigation","tables","Definitions","identifier","controllerConstructor","Controller","static","connect","navigator","this","element","classList","add","supportedClass","copy","event","preventDefault","clipboard","writeText","sourceTarget","value","setTimeout","remove","close","e","target","closest","children","length","mapping","String","depth","type","Number","default","cause","HTMLElement","name","nodeName","toLowerCase","getAttribute","isContentEditable","isFormField","ignore","key","describeEvent","buffer","slice","depthValue","action","reduceRight","mappings","CustomEvent","detail","bubbles","dispatchEvent","ctrlKey","metaKey","altKey","shiftKey","code","filter","w","join","inputs","mappingValue","replaceAll","split","f","forEach","parse","Object","defineProperty","writable","pattern","keys","first","shift","addEventListener","onSubmit","disconnect","removeEventListener","outside","tagName","dismiss","dialogTarget","open","removeAttribute","dialogTargetConnected","dialog","showModal","success","formSubmission","submitter","dataset","filterTarget","clearFilter","applyFilter","go","querySelector","click","clear","blur","links","li","prefixSearch","innerText","toggleAttribute","menus","matches","querySelectorAll","needle","haystack","haystackLength","needleLength","outer","i","j","needleChar","charCodeAt","toggle","onMorphAttribute","attributeName","Array","from","trigger","dispatch","prefix","document","prevPage","nextPage","slug","sourceChanged","slugValue","slugTarget","replace","slugChanged","options","authenticate","credential","credentials","get","responseTarget","JSON","stringify","toJSON","requestSubmit","publicKey","PublicKeyCredential","parseRequestOptionsFromJSON","optionsValue","submit","createCredential","then","create","parseCreationOptionsFromJSON","import","HwComboboxController","push","catch","KoiToolbarElement","constructor","super","setAttribute","initGOVUK","body","HTMLScriptElement","prototype","initAll","customElements","define","window","Turbo"],"mappings":"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"}