@ministryofjustice/frontend 3.5.0 → 3.6.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.
Files changed (36) hide show
  1. package/moj/all.jquery.js +13378 -0
  2. package/moj/all.jquery.min.js +1 -81
  3. package/moj/all.js +2577 -2853
  4. package/moj/all.mjs +126 -0
  5. package/moj/components/add-another/add-another.js +111 -132
  6. package/moj/components/add-another/add-another.mjs +106 -0
  7. package/moj/components/alert/alert.js +352 -479
  8. package/moj/components/alert/alert.mjs +251 -0
  9. package/moj/components/alert/alert.spec.helper.js +6 -24
  10. package/moj/components/alert/alert.spec.helper.mjs +66 -0
  11. package/moj/components/button-menu/button-menu.js +326 -343
  12. package/moj/components/button-menu/button-menu.mjs +329 -0
  13. package/moj/components/date-picker/date-picker.js +905 -922
  14. package/moj/components/date-picker/date-picker.mjs +961 -0
  15. package/moj/components/filter-toggle-button/filter-toggle-button.js +98 -119
  16. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +93 -0
  17. package/moj/components/form-validator/form-validator.js +201 -396
  18. package/moj/components/form-validator/form-validator.mjs +168 -0
  19. package/moj/components/multi-file-upload/multi-file-upload.js +227 -441
  20. package/moj/components/multi-file-upload/multi-file-upload.mjs +219 -0
  21. package/moj/components/multi-select/multi-select.js +82 -103
  22. package/moj/components/multi-select/multi-select.mjs +77 -0
  23. package/moj/components/password-reveal/password-reveal.js +40 -61
  24. package/moj/components/password-reveal/password-reveal.mjs +35 -0
  25. package/moj/components/rich-text-editor/rich-text-editor.js +162 -183
  26. package/moj/components/rich-text-editor/rich-text-editor.mjs +157 -0
  27. package/moj/components/search-toggle/search-toggle.js +52 -73
  28. package/moj/components/search-toggle/search-toggle.mjs +54 -0
  29. package/moj/components/sortable-table/sortable-table.js +143 -164
  30. package/moj/components/sortable-table/sortable-table.mjs +138 -0
  31. package/moj/helpers.js +196 -215
  32. package/moj/helpers.mjs +123 -0
  33. package/moj/moj-frontend.min.js +1 -81
  34. package/moj/version.js +6 -23
  35. package/moj/version.mjs +3 -0
  36. package/package.json +13 -1
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @typedef {object} ButtonMenuConfig
3
+ * @property {string} [buttonText=Actions] - Label for the toggle button
4
+ * @property {"left" | "right"} [alignMenu=left] - the alignment of the menu
5
+ * @property {string} [buttonClasses=govuk-button--secondary] - css classes applied to the toggle button
6
+ */
7
+
8
+ /**
9
+ * @param {HTMLElement} $module
10
+ * @param {ButtonMenuConfig} config
11
+ * @class
12
+ */
13
+ function ButtonMenu($module, config = {}) {
14
+ if (!$module) {
15
+ return this
16
+ }
17
+
18
+ const schema = Object.freeze({
19
+ properties: {
20
+ buttonText: { type: 'string' },
21
+ buttonClasses: { type: 'string' },
22
+ alignMenu: { type: 'string' }
23
+ }
24
+ });
25
+
26
+ const defaults = {
27
+ buttonText: 'Actions',
28
+ alignMenu: 'left',
29
+ buttonClasses: ''
30
+ };
31
+ // data attributes override JS config, which overrides defaults
32
+ this.config = this.mergeConfigs(
33
+ defaults,
34
+ config,
35
+ this.parseDataset(schema, $module.dataset)
36
+ );
37
+
38
+ this.$module = $module;
39
+ }
40
+
41
+ ButtonMenu.prototype.init = function () {
42
+ // If only one button is provided, don't initiate a menu and toggle button
43
+ // if classes have been provided for the toggleButton, apply them to the single item
44
+ if (this.$module.children.length === 1) {
45
+ const button = this.$module.children[0];
46
+ button.classList.forEach((className) => {
47
+ if (className.startsWith('govuk-button-')) {
48
+ button.classList.remove(className);
49
+ }
50
+ button.classList.remove('moj-button-menu__item');
51
+ });
52
+ if (this.config.buttonClasses) {
53
+ button.classList.add(...this.config.buttonClasses.split(' '));
54
+ }
55
+ }
56
+ // Otherwise intialise a button menu
57
+ if (this.$module.children.length > 1) {
58
+ this.initMenu();
59
+ }
60
+ };
61
+
62
+ ButtonMenu.prototype.initMenu = function () {
63
+ this.$menu = this.createMenu();
64
+ this.$module.insertAdjacentHTML('afterbegin', this.toggleTemplate());
65
+ this.setupMenuItems();
66
+
67
+ this.$menuToggle = this.$module.querySelector(':scope > button');
68
+ this.items = this.$menu.querySelectorAll('a, button');
69
+
70
+ this.$menuToggle.addEventListener('click', (event) => {
71
+ this.toggleMenu(event);
72
+ });
73
+
74
+ this.$module.addEventListener('keydown', (event) => {
75
+ this.handleKeyDown(event);
76
+ });
77
+
78
+ document.addEventListener('click', (event) => {
79
+ if (!this.$module.contains(event.target)) {
80
+ this.closeMenu(false);
81
+ }
82
+ });
83
+ };
84
+
85
+ ButtonMenu.prototype.createMenu = function () {
86
+ const $menu = document.createElement('ul');
87
+ $menu.setAttribute('role', 'list');
88
+ $menu.hidden = true;
89
+ $menu.classList.add('moj-button-menu__wrapper');
90
+ if (this.config.alignMenu === 'right') {
91
+ $menu.classList.add('moj-button-menu__wrapper--right');
92
+ }
93
+
94
+ this.$module.appendChild($menu);
95
+ while (this.$module.firstChild !== $menu) {
96
+ $menu.appendChild(this.$module.firstChild);
97
+ }
98
+
99
+ return $menu
100
+ };
101
+
102
+ ButtonMenu.prototype.setupMenuItems = function () {
103
+ Array.from(this.$menu.children).forEach((item) => {
104
+ // wrap item in li tag
105
+ const listItem = document.createElement('li');
106
+ this.$menu.insertBefore(listItem, item);
107
+ listItem.appendChild(item);
108
+
109
+ item.setAttribute('tabindex', -1);
110
+
111
+ if (item.tagName === 'BUTTON') {
112
+ item.setAttribute('type', 'button');
113
+ }
114
+
115
+ item.classList.forEach((className) => {
116
+ if (className.startsWith('govuk-button')) {
117
+ item.classList.remove(className);
118
+ }
119
+ });
120
+
121
+ // add a slight delay after click before closing the menu, makes it *feel* better
122
+ item.addEventListener('click', (event) => {
123
+ setTimeout(() => {
124
+ this.closeMenu(false);
125
+ }, 50);
126
+ });
127
+ });
128
+ };
129
+
130
+ ButtonMenu.prototype.toggleTemplate = function () {
131
+ return `
132
+ <button type="button" class="govuk-button moj-button-menu__toggle-button ${this.config.buttonClasses || ''}" aria-haspopup="true" aria-expanded="false">
133
+ <span>
134
+ ${this.config.buttonText}
135
+ <svg width="11" height="5" viewBox="0 0 11 5" xmlns="http://www.w3.org/2000/svg">
136
+ <path d="M5.5 0L11 5L0 5L5.5 0Z" fill="currentColor"/>
137
+ </svg>
138
+ </span>
139
+ </button>`
140
+ };
141
+
142
+ /**
143
+ * @returns {boolean}
144
+ */
145
+ ButtonMenu.prototype.isOpen = function () {
146
+ return this.$menuToggle.getAttribute('aria-expanded') === 'true'
147
+ };
148
+
149
+ ButtonMenu.prototype.toggleMenu = function (event) {
150
+ event.preventDefault();
151
+
152
+ // If menu is triggered with mouse don't move focus to first item
153
+ const keyboardEvent = event.detail === 0;
154
+ const focusIndex = keyboardEvent ? 0 : -1;
155
+
156
+ if (this.isOpen()) {
157
+ this.closeMenu();
158
+ } else {
159
+ this.openMenu(focusIndex);
160
+ }
161
+ };
162
+
163
+ /**
164
+ * Opens the menu and optionally sets the focus to the item with given index
165
+ *
166
+ * @param {number} focusIndex - The index of the item to focus
167
+ */
168
+ ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
169
+ this.$menu.hidden = false;
170
+ this.$menuToggle.setAttribute('aria-expanded', 'true');
171
+ if (focusIndex !== -1) {
172
+ this.focusItem(focusIndex);
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Closes the menu and optionally returns focus back to menuToggle
178
+ *
179
+ * @param {boolean} moveFocus - whether to return focus to the toggle button
180
+ */
181
+ ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
182
+ this.$menu.hidden = true;
183
+ this.$menuToggle.setAttribute('aria-expanded', 'false');
184
+ if (moveFocus) {
185
+ this.$menuToggle.focus();
186
+ }
187
+ };
188
+
189
+ /**
190
+ * Focuses the menu item at the specified index
191
+ *
192
+ * @param {number} index - the index of the item to focus
193
+ */
194
+ ButtonMenu.prototype.focusItem = function (index) {
195
+ if (index >= this.items.length) index = 0;
196
+ if (index < 0) index = this.items.length - 1;
197
+
198
+ const menuItem = this.items.item(index);
199
+ if (menuItem) {
200
+ menuItem.focus();
201
+ }
202
+ };
203
+
204
+ ButtonMenu.prototype.currentFocusIndex = function () {
205
+ const activeElement = document.activeElement;
206
+ const menuItems = Array.from(this.items);
207
+
208
+ return menuItems.indexOf(activeElement)
209
+ };
210
+
211
+ ButtonMenu.prototype.handleKeyDown = function (event) {
212
+ if (event.target === this.$menuToggle) {
213
+ switch (event.key) {
214
+ case 'ArrowDown':
215
+ event.preventDefault();
216
+ this.openMenu();
217
+ break
218
+ case 'ArrowUp':
219
+ event.preventDefault();
220
+ this.openMenu(this.items.length - 1);
221
+ break
222
+ }
223
+ }
224
+
225
+ if (this.$menu.contains(event.target) && this.isOpen()) {
226
+ switch (event.key) {
227
+ case 'ArrowDown':
228
+ event.preventDefault();
229
+ if (this.currentFocusIndex() !== -1) {
230
+ this.focusItem(this.currentFocusIndex() + 1);
231
+ }
232
+ break
233
+ case 'ArrowUp':
234
+ event.preventDefault();
235
+ if (this.currentFocusIndex() !== -1) {
236
+ this.focusItem(this.currentFocusIndex() - 1);
237
+ }
238
+ break
239
+ case 'Home':
240
+ event.preventDefault();
241
+ this.focusItem(0);
242
+ break
243
+ case 'End':
244
+ event.preventDefault();
245
+ this.focusItem(this.items.length - 1);
246
+ break
247
+ }
248
+ }
249
+
250
+ if (event.key === 'Escape' && this.isOpen()) {
251
+ this.closeMenu();
252
+ }
253
+ if (event.key === 'Tab' && this.isOpen()) {
254
+ this.closeMenu(false);
255
+ }
256
+ };
257
+
258
+ /**
259
+ * Parse dataset
260
+ *
261
+ * Loop over an object and normalise each value using {@link normaliseString},
262
+ * optionally expanding nested `i18n.field`
263
+ *
264
+ * @param {Schema} schema - component schema
265
+ * @param {DOMStringMap} dataset - HTML element dataset
266
+ * @returns {object} Normalised dataset
267
+ */
268
+ ButtonMenu.prototype.parseDataset = function (schema, dataset) {
269
+ const parsed = {};
270
+
271
+ for (const [field, ,] of Object.entries(schema.properties)) {
272
+ if (field in dataset) {
273
+ if (dataset[field]) {
274
+ parsed[field] = dataset[field];
275
+ }
276
+ }
277
+ }
278
+
279
+ return parsed
280
+ };
281
+
282
+ /**
283
+ * Config merging function
284
+ *
285
+ * Takes any number of objects and combines them together, with
286
+ * greatest priority on the LAST item passed in.
287
+ *
288
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
289
+ * @returns {{ [key: string]: unknown }} A merged config object
290
+ */
291
+ ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
292
+ const formattedConfigObject = {};
293
+
294
+ // Loop through each of the passed objects
295
+ for (const configObject of configObjects) {
296
+ for (const key of Object.keys(configObject)) {
297
+ const option = formattedConfigObject[key];
298
+ const override = configObject[key];
299
+
300
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
301
+ // keys with object values will be merged, otherwise the new value will
302
+ // override the existing value.
303
+ if (typeof option === 'object' && typeof override === 'object') {
304
+ // @ts-expect-error Index signature for type 'string' is missing
305
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
306
+ } else {
307
+ formattedConfigObject[key] = override;
308
+ }
309
+ }
310
+ }
311
+
312
+ return formattedConfigObject
313
+ };
314
+
315
+ /**
316
+ * Schema for component config
317
+ *
318
+ * @typedef {object} Schema
319
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
320
+ */
321
+
322
+ /**
323
+ * Schema property for component config
324
+ *
325
+ * @typedef {object} SchemaProperty
326
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
327
+ */
328
+
329
+ export { ButtonMenu };