@ministryofjustice/frontend 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/moj/all.bundle.js +1549 -1062
- package/moj/all.bundle.js.map +1 -1
- package/moj/all.bundle.mjs +1845 -1054
- package/moj/all.bundle.mjs.map +1 -1
- package/moj/all.mjs +7 -90
- package/moj/all.mjs.map +1 -1
- package/moj/all.scss +1 -0
- package/moj/all.scss.map +1 -1
- package/moj/common/index.mjs +57 -0
- package/moj/common/index.mjs.map +1 -0
- package/moj/common/moj-frontend-version.mjs +14 -0
- package/moj/common/moj-frontend-version.mjs.map +1 -0
- package/moj/components/add-another/add-another.bundle.js +105 -76
- package/moj/components/add-another/add-another.bundle.js.map +1 -1
- package/moj/components/add-another/add-another.bundle.mjs +222 -71
- package/moj/components/add-another/add-another.bundle.mjs.map +1 -1
- package/moj/components/add-another/add-another.mjs +103 -72
- package/moj/components/add-another/add-another.mjs.map +1 -1
- package/moj/components/alert/alert.bundle.js +115 -191
- package/moj/components/alert/alert.bundle.js.map +1 -1
- package/moj/components/alert/alert.bundle.mjs +354 -186
- package/moj/components/alert/alert.bundle.mjs.map +1 -1
- package/moj/components/alert/alert.mjs +55 -140
- package/moj/components/alert/alert.mjs.map +1 -1
- package/moj/components/button-menu/README.md +3 -1
- package/moj/components/button-menu/button-menu.bundle.js +91 -120
- package/moj/components/button-menu/button-menu.bundle.js.map +1 -1
- package/moj/components/button-menu/button-menu.bundle.mjs +329 -114
- package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -1
- package/moj/components/button-menu/button-menu.mjs +89 -116
- package/moj/components/button-menu/button-menu.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.js +174 -154
- package/moj/components/date-picker/date-picker.bundle.js.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.mjs +411 -147
- package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.mjs +172 -150
- package/moj/components/date-picker/date-picker.mjs.map +1 -1
- package/moj/components/filter/template.njk +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +133 -44
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +374 -41
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs +131 -40
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.js +159 -69
- package/moj/components/form-validator/form-validator.bundle.js.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.mjs +399 -65
- package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.mjs +134 -54
- package/moj/components/form-validator/form-validator.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js +291 -117
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +527 -109
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.mjs +288 -101
- package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
- package/moj/components/multi-file-upload/template.njk +1 -1
- package/moj/components/multi-select/multi-select.bundle.js +106 -41
- package/moj/components/multi-select/multi-select.bundle.js.map +1 -1
- package/moj/components/multi-select/multi-select.bundle.mjs +346 -37
- package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -1
- package/moj/components/multi-select/multi-select.mjs +104 -37
- package/moj/components/multi-select/multi-select.mjs.map +1 -1
- package/moj/components/password-reveal/_password-reveal.scss +3 -1
- package/moj/components/password-reveal/_password-reveal.scss.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.js +32 -29
- package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.mjs +149 -24
- package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -1
- package/moj/components/password-reveal/password-reveal.mjs +30 -25
- package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
- package/moj/components/rich-text-editor/README.md +4 -3
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js +127 -62
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +367 -58
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.mjs +125 -58
- package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.js +94 -26
- package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.mjs +334 -22
- package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.mjs +92 -22
- package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.js +151 -83
- package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.mjs +390 -78
- package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.mjs +149 -79
- package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
- package/moj/core/_all.scss +3 -0
- package/moj/core/_all.scss.map +1 -0
- package/moj/core/_moj-frontend-properties.scss +7 -0
- package/moj/core/_moj-frontend-properties.scss.map +1 -0
- package/moj/filters/prototype-kit-13-filters.js +4 -3
- package/moj/helpers.bundle.js +22 -77
- package/moj/helpers.bundle.js.map +1 -1
- package/moj/helpers.bundle.mjs +23 -74
- package/moj/helpers.bundle.mjs.map +1 -1
- package/moj/helpers.mjs +23 -74
- package/moj/helpers.mjs.map +1 -1
- package/moj/moj-frontend.min.css +1 -1
- package/moj/moj-frontend.min.css.map +1 -1
- package/moj/moj-frontend.min.js +1 -1
- package/moj/moj-frontend.min.js.map +1 -1
- package/package.json +1 -1
- package/moj/version.bundle.js +0 -12
- package/moj/version.bundle.js.map +0 -1
- package/moj/version.bundle.mjs +0 -4
- package/moj/version.bundle.mjs.map +0 -1
- package/moj/version.mjs +0 -4
- package/moj/version.mjs.map +0 -1
|
@@ -1,68 +1,292 @@
|
|
|
1
|
-
|
|
1
|
+
function isInitialised($root, moduleName) {
|
|
2
|
+
return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if GOV.UK Frontend is supported on this page
|
|
7
|
+
*
|
|
8
|
+
* Some browsers will load and run our JavaScript but GOV.UK Frontend
|
|
9
|
+
* won't be supported.
|
|
10
|
+
*
|
|
11
|
+
* @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
|
|
12
|
+
* @returns {boolean} Whether GOV.UK Frontend is supported on this page
|
|
13
|
+
*/
|
|
14
|
+
function isSupported($scope = document.body) {
|
|
15
|
+
if (!$scope) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return $scope.classList.contains('govuk-frontend-supported');
|
|
19
|
+
}
|
|
20
|
+
function isArray(option) {
|
|
21
|
+
return Array.isArray(option);
|
|
22
|
+
}
|
|
23
|
+
function isObject(option) {
|
|
24
|
+
return !!option && typeof option === 'object' && !isArray(option);
|
|
25
|
+
}
|
|
26
|
+
function formatErrorMessage(Component, message) {
|
|
27
|
+
return `${Component.moduleName}: ${message}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class GOVUKFrontendError extends Error {
|
|
31
|
+
constructor(...args) {
|
|
32
|
+
super(...args);
|
|
33
|
+
this.name = 'GOVUKFrontendError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class SupportError extends GOVUKFrontendError {
|
|
2
37
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
38
|
+
* Checks if GOV.UK Frontend is supported on this page
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
|
|
41
|
+
*/
|
|
42
|
+
constructor($scope = document.body) {
|
|
43
|
+
const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
|
|
44
|
+
super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
|
|
45
|
+
this.name = 'SupportError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class ConfigError extends GOVUKFrontendError {
|
|
49
|
+
constructor(...args) {
|
|
50
|
+
super(...args);
|
|
51
|
+
this.name = 'ConfigError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class ElementError extends GOVUKFrontendError {
|
|
55
|
+
constructor(messageOrOptions) {
|
|
56
|
+
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
|
|
57
|
+
if (typeof messageOrOptions === 'object') {
|
|
58
|
+
const {
|
|
59
|
+
component,
|
|
60
|
+
identifier,
|
|
61
|
+
element,
|
|
62
|
+
expectedType
|
|
63
|
+
} = messageOrOptions;
|
|
64
|
+
message = identifier;
|
|
65
|
+
message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
|
|
66
|
+
message = formatErrorMessage(component, message);
|
|
67
|
+
}
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'ElementError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class InitError extends GOVUKFrontendError {
|
|
73
|
+
constructor(componentOrMessage) {
|
|
74
|
+
const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'InitError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class Component {
|
|
81
|
+
/**
|
|
82
|
+
* Returns the root element of the component
|
|
83
|
+
*
|
|
84
|
+
* @protected
|
|
85
|
+
* @returns {RootElementType} - the root element of component
|
|
86
|
+
*/
|
|
87
|
+
get $root() {
|
|
88
|
+
return this._$root;
|
|
89
|
+
}
|
|
90
|
+
constructor($root) {
|
|
91
|
+
this._$root = void 0;
|
|
92
|
+
const childConstructor = this.constructor;
|
|
93
|
+
if (typeof childConstructor.moduleName !== 'string') {
|
|
94
|
+
throw new InitError(`\`moduleName\` not defined in component`);
|
|
95
|
+
}
|
|
96
|
+
if (!($root instanceof childConstructor.elementType)) {
|
|
97
|
+
throw new ElementError({
|
|
98
|
+
element: $root,
|
|
99
|
+
component: childConstructor,
|
|
100
|
+
identifier: 'Root element (`$root`)',
|
|
101
|
+
expectedType: childConstructor.elementType.name
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
this._$root = $root;
|
|
105
|
+
}
|
|
106
|
+
childConstructor.checkSupport();
|
|
107
|
+
this.checkInitialised();
|
|
108
|
+
const moduleName = childConstructor.moduleName;
|
|
109
|
+
this.$root.setAttribute(`data-${moduleName}-init`, '');
|
|
110
|
+
}
|
|
111
|
+
checkInitialised() {
|
|
112
|
+
const constructor = this.constructor;
|
|
113
|
+
const moduleName = constructor.moduleName;
|
|
114
|
+
if (moduleName && isInitialised(this.$root, moduleName)) {
|
|
115
|
+
throw new InitError(constructor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
static checkSupport() {
|
|
119
|
+
if (!isSupported()) {
|
|
120
|
+
throw new SupportError();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @typedef ChildClass
|
|
127
|
+
* @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @typedef {typeof Component & ChildClass} ChildClassConstructor
|
|
132
|
+
*/
|
|
133
|
+
Component.elementType = HTMLElement;
|
|
134
|
+
|
|
135
|
+
const configOverride = Symbol.for('configOverride');
|
|
136
|
+
class ConfigurableComponent extends Component {
|
|
137
|
+
[configOverride](param) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the root element of the component
|
|
143
|
+
*
|
|
144
|
+
* @protected
|
|
145
|
+
* @returns {ConfigurationType} - the root element of component
|
|
5
146
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
147
|
+
get config() {
|
|
148
|
+
return this._config;
|
|
149
|
+
}
|
|
150
|
+
constructor($root, config) {
|
|
151
|
+
super($root);
|
|
152
|
+
this._config = void 0;
|
|
153
|
+
const childConstructor = this.constructor;
|
|
154
|
+
if (!isObject(childConstructor.defaults)) {
|
|
155
|
+
throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
|
|
9
156
|
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
157
|
+
const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
|
|
158
|
+
this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function normaliseString(value, property) {
|
|
162
|
+
const trimmedValue = value ? value.trim() : '';
|
|
163
|
+
let output;
|
|
164
|
+
let outputType = property == null ? void 0 : property.type;
|
|
165
|
+
if (!outputType) {
|
|
166
|
+
if (['true', 'false'].includes(trimmedValue)) {
|
|
167
|
+
outputType = 'boolean';
|
|
168
|
+
}
|
|
169
|
+
if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
|
|
170
|
+
outputType = 'number';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
switch (outputType) {
|
|
174
|
+
case 'boolean':
|
|
175
|
+
output = trimmedValue === 'true';
|
|
176
|
+
break;
|
|
177
|
+
case 'number':
|
|
178
|
+
output = Number(trimmedValue);
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
output = value;
|
|
182
|
+
}
|
|
183
|
+
return output;
|
|
184
|
+
}
|
|
185
|
+
function normaliseDataset(Component, dataset) {
|
|
186
|
+
if (!isObject(Component.schema)) {
|
|
187
|
+
throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
|
|
188
|
+
}
|
|
189
|
+
const out = {};
|
|
190
|
+
const entries = Object.entries(Component.schema.properties);
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const [namespace, property] = entry;
|
|
193
|
+
const field = namespace.toString();
|
|
194
|
+
if (field in dataset) {
|
|
195
|
+
out[field] = normaliseString(dataset[field], property);
|
|
196
|
+
}
|
|
197
|
+
if ((property == null ? void 0 : property.type) === 'object') {
|
|
198
|
+
out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
function mergeConfigs(...configObjects) {
|
|
204
|
+
const formattedConfigObject = {};
|
|
205
|
+
for (const configObject of configObjects) {
|
|
206
|
+
for (const key of Object.keys(configObject)) {
|
|
207
|
+
const option = formattedConfigObject[key];
|
|
208
|
+
const override = configObject[key];
|
|
209
|
+
if (isObject(option) && isObject(override)) {
|
|
210
|
+
formattedConfigObject[key] = mergeConfigs(option, override);
|
|
211
|
+
} else {
|
|
212
|
+
formattedConfigObject[key] = override;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return formattedConfigObject;
|
|
217
|
+
}
|
|
218
|
+
function extractConfigByNamespace(schema, dataset, namespace) {
|
|
219
|
+
const property = schema.properties[namespace];
|
|
220
|
+
if ((property == null ? void 0 : property.type) !== 'object') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const newObject = {
|
|
224
|
+
[namespace]: {}
|
|
225
|
+
};
|
|
226
|
+
for (const [key, value] of Object.entries(dataset)) {
|
|
227
|
+
let current = newObject;
|
|
228
|
+
const keyParts = key.split('.');
|
|
229
|
+
for (const [index, name] of keyParts.entries()) {
|
|
230
|
+
if (isObject(current)) {
|
|
231
|
+
if (index < keyParts.length - 1) {
|
|
232
|
+
if (!isObject(current[name])) {
|
|
233
|
+
current[name] = {};
|
|
234
|
+
}
|
|
235
|
+
current = current[name];
|
|
236
|
+
} else if (key !== namespace) {
|
|
237
|
+
current[name] = normaliseString(value);
|
|
20
238
|
}
|
|
21
239
|
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return newObject[namespace];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @augments {ConfigurableComponent<ButtonMenuConfig>}
|
|
247
|
+
*/
|
|
248
|
+
class ButtonMenu extends ConfigurableComponent {
|
|
249
|
+
/**
|
|
250
|
+
* @param {Element | null} $root - HTML element to use for button menu
|
|
251
|
+
* @param {ButtonMenuConfig} [config] - Button menu config
|
|
252
|
+
*/
|
|
253
|
+
constructor($root, config = {}) {
|
|
254
|
+
super($root, config);
|
|
31
255
|
|
|
32
256
|
// If only one button is provided, don't initiate a menu and toggle button
|
|
33
257
|
// if classes have been provided for the toggleButton, apply them to the single item
|
|
34
|
-
if (this.$
|
|
35
|
-
const button = this.$
|
|
36
|
-
button.classList.forEach(className => {
|
|
258
|
+
if (this.$root.children.length === 1) {
|
|
259
|
+
const $button = this.$root.children[0];
|
|
260
|
+
$button.classList.forEach(className => {
|
|
37
261
|
if (className.startsWith('govuk-button-')) {
|
|
38
|
-
button.classList.remove(className);
|
|
262
|
+
$button.classList.remove(className);
|
|
39
263
|
}
|
|
40
|
-
button.classList.remove('moj-button-menu__item');
|
|
41
|
-
button.classList.add('moj-button-menu__single-button');
|
|
264
|
+
$button.classList.remove('moj-button-menu__item');
|
|
265
|
+
$button.classList.add('moj-button-menu__single-button');
|
|
42
266
|
});
|
|
43
267
|
if (this.config.buttonClasses) {
|
|
44
|
-
button.classList.add(...this.config.buttonClasses.split(' '));
|
|
268
|
+
$button.classList.add(...this.config.buttonClasses.split(' '));
|
|
45
269
|
}
|
|
46
270
|
}
|
|
47
|
-
// Otherwise
|
|
48
|
-
if (this.$
|
|
271
|
+
// Otherwise initialise a button menu
|
|
272
|
+
if (this.$root.children.length > 1) {
|
|
49
273
|
this.initMenu();
|
|
50
274
|
}
|
|
51
275
|
}
|
|
52
276
|
initMenu() {
|
|
53
277
|
this.$menu = this.createMenu();
|
|
54
|
-
this.$
|
|
278
|
+
this.$root.insertAdjacentHTML('afterbegin', this.toggleTemplate());
|
|
55
279
|
this.setupMenuItems();
|
|
56
|
-
this.$menuToggle = this.$
|
|
57
|
-
this
|
|
280
|
+
this.$menuToggle = this.$root.querySelector(':scope > button');
|
|
281
|
+
this.$items = this.$menu.querySelectorAll('a, button');
|
|
58
282
|
this.$menuToggle.addEventListener('click', event => {
|
|
59
283
|
this.toggleMenu(event);
|
|
60
284
|
});
|
|
61
|
-
this.$
|
|
285
|
+
this.$root.addEventListener('keydown', event => {
|
|
62
286
|
this.handleKeyDown(event);
|
|
63
287
|
});
|
|
64
288
|
document.addEventListener('click', event => {
|
|
65
|
-
if (!this.$
|
|
289
|
+
if (event.target instanceof Node && !this.$root.contains(event.target)) {
|
|
66
290
|
this.closeMenu(false);
|
|
67
291
|
}
|
|
68
292
|
});
|
|
@@ -75,30 +299,30 @@ class ButtonMenu {
|
|
|
75
299
|
if (this.config.alignMenu === 'right') {
|
|
76
300
|
$menu.classList.add('moj-button-menu__wrapper--right');
|
|
77
301
|
}
|
|
78
|
-
this.$
|
|
79
|
-
while (this.$
|
|
80
|
-
$menu.appendChild(this.$
|
|
302
|
+
this.$root.appendChild($menu);
|
|
303
|
+
while (this.$root.firstChild !== $menu) {
|
|
304
|
+
$menu.appendChild(this.$root.firstChild);
|
|
81
305
|
}
|
|
82
306
|
return $menu;
|
|
83
307
|
}
|
|
84
308
|
setupMenuItems() {
|
|
85
|
-
Array.from(this.$menu.children).forEach(
|
|
309
|
+
Array.from(this.$menu.children).forEach($menuItem => {
|
|
86
310
|
// wrap item in li tag
|
|
87
|
-
const listItem = document.createElement('li');
|
|
88
|
-
this.$menu.insertBefore(listItem,
|
|
89
|
-
listItem.appendChild(
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
311
|
+
const $listItem = document.createElement('li');
|
|
312
|
+
this.$menu.insertBefore($listItem, $menuItem);
|
|
313
|
+
$listItem.appendChild($menuItem);
|
|
314
|
+
$menuItem.setAttribute('tabindex', '-1');
|
|
315
|
+
if ($menuItem.tagName === 'BUTTON') {
|
|
316
|
+
$menuItem.setAttribute('type', 'button');
|
|
93
317
|
}
|
|
94
|
-
|
|
318
|
+
$menuItem.classList.forEach(className => {
|
|
95
319
|
if (className.startsWith('govuk-button')) {
|
|
96
|
-
|
|
320
|
+
$menuItem.classList.remove(className);
|
|
97
321
|
}
|
|
98
322
|
});
|
|
99
323
|
|
|
100
324
|
// add a slight delay after click before closing the menu, makes it *feel* better
|
|
101
|
-
|
|
325
|
+
$menuItem.addEventListener('click', () => {
|
|
102
326
|
setTimeout(() => {
|
|
103
327
|
this.closeMenu(false);
|
|
104
328
|
}, 50);
|
|
@@ -123,6 +347,10 @@ class ButtonMenu {
|
|
|
123
347
|
isOpen() {
|
|
124
348
|
return this.$menuToggle.getAttribute('aria-expanded') === 'true';
|
|
125
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {MouseEvent} event - Click event
|
|
353
|
+
*/
|
|
126
354
|
toggleMenu(event) {
|
|
127
355
|
event.preventDefault();
|
|
128
356
|
|
|
@@ -168,18 +396,22 @@ class ButtonMenu {
|
|
|
168
396
|
* @param {number} index - the index of the item to focus
|
|
169
397
|
*/
|
|
170
398
|
focusItem(index) {
|
|
171
|
-
if (index >= this
|
|
172
|
-
if (index < 0) index = this
|
|
173
|
-
const menuItem = this
|
|
174
|
-
if (menuItem) {
|
|
175
|
-
menuItem.focus();
|
|
399
|
+
if (index >= this.$items.length) index = 0;
|
|
400
|
+
if (index < 0) index = this.$items.length - 1;
|
|
401
|
+
const $menuItem = this.$items.item(index);
|
|
402
|
+
if ($menuItem) {
|
|
403
|
+
$menuItem.focus();
|
|
176
404
|
}
|
|
177
405
|
}
|
|
178
406
|
currentFocusIndex() {
|
|
179
|
-
const activeElement = document.activeElement;
|
|
180
|
-
const menuItems = Array.from(this
|
|
181
|
-
return menuItems.indexOf(activeElement);
|
|
407
|
+
const $activeElement = document.activeElement;
|
|
408
|
+
const $menuItems = Array.from(this.$items);
|
|
409
|
+
return ($activeElement instanceof HTMLAnchorElement || $activeElement instanceof HTMLButtonElement) && $menuItems.indexOf($activeElement);
|
|
182
410
|
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
414
|
+
*/
|
|
183
415
|
handleKeyDown(event) {
|
|
184
416
|
if (event.target === this.$menuToggle) {
|
|
185
417
|
switch (event.key) {
|
|
@@ -189,11 +421,11 @@ class ButtonMenu {
|
|
|
189
421
|
break;
|
|
190
422
|
case 'ArrowUp':
|
|
191
423
|
event.preventDefault();
|
|
192
|
-
this.openMenu(this
|
|
424
|
+
this.openMenu(this.$items.length - 1);
|
|
193
425
|
break;
|
|
194
426
|
}
|
|
195
427
|
}
|
|
196
|
-
if (this.$menu.contains(event.target) && this.isOpen()) {
|
|
428
|
+
if (event.target instanceof Node && this.$menu.contains(event.target) && this.isOpen()) {
|
|
197
429
|
switch (event.key) {
|
|
198
430
|
case 'ArrowDown':
|
|
199
431
|
event.preventDefault();
|
|
@@ -213,7 +445,7 @@ class ButtonMenu {
|
|
|
213
445
|
break;
|
|
214
446
|
case 'End':
|
|
215
447
|
event.preventDefault();
|
|
216
|
-
this.focusItem(this
|
|
448
|
+
this.focusItem(this.$items.length - 1);
|
|
217
449
|
break;
|
|
218
450
|
}
|
|
219
451
|
}
|
|
@@ -226,58 +458,8 @@ class ButtonMenu {
|
|
|
226
458
|
}
|
|
227
459
|
|
|
228
460
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* Loop over an object and normalise each value using {@link normaliseString},
|
|
232
|
-
* optionally expanding nested `i18n.field`
|
|
233
|
-
*
|
|
234
|
-
* @param {Schema} schema - component schema
|
|
235
|
-
* @param {DOMStringMap} dataset - HTML element dataset
|
|
236
|
-
* @returns {object} Normalised dataset
|
|
237
|
-
*/
|
|
238
|
-
parseDataset(schema, dataset) {
|
|
239
|
-
const parsed = {};
|
|
240
|
-
for (const [field,,] of Object.entries(schema.properties)) {
|
|
241
|
-
if (field in dataset) {
|
|
242
|
-
if (dataset[field]) {
|
|
243
|
-
parsed[field] = dataset[field];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return parsed;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Config merging function
|
|
252
|
-
*
|
|
253
|
-
* Takes any number of objects and combines them together, with
|
|
254
|
-
* greatest priority on the LAST item passed in.
|
|
255
|
-
*
|
|
256
|
-
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
257
|
-
* @returns {{ [key: string]: unknown }} A merged config object
|
|
461
|
+
* Name for the component used when initialising using data-module attributes.
|
|
258
462
|
*/
|
|
259
|
-
mergeConfigs(...configObjects) {
|
|
260
|
-
const formattedConfigObject = {};
|
|
261
|
-
|
|
262
|
-
// Loop through each of the passed objects
|
|
263
|
-
for (const configObject of configObjects) {
|
|
264
|
-
for (const key of Object.keys(configObject)) {
|
|
265
|
-
const option = formattedConfigObject[key];
|
|
266
|
-
const override = configObject[key];
|
|
267
|
-
|
|
268
|
-
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
269
|
-
// keys with object values will be merged, otherwise the new value will
|
|
270
|
-
// override the existing value.
|
|
271
|
-
if (typeof option === 'object' && typeof override === 'object') {
|
|
272
|
-
// @ts-expect-error Index signature for type 'string' is missing
|
|
273
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
274
|
-
} else {
|
|
275
|
-
formattedConfigObject[key] = override;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return formattedConfigObject;
|
|
280
|
-
}
|
|
281
463
|
}
|
|
282
464
|
|
|
283
465
|
/**
|
|
@@ -287,5 +469,38 @@ class ButtonMenu {
|
|
|
287
469
|
* @property {string} [buttonClasses='govuk-button--secondary'] - css classes applied to the toggle button
|
|
288
470
|
*/
|
|
289
471
|
|
|
472
|
+
/**
|
|
473
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
474
|
+
*/
|
|
475
|
+
ButtonMenu.moduleName = 'moj-button-menu';
|
|
476
|
+
/**
|
|
477
|
+
* Button menu config
|
|
478
|
+
*
|
|
479
|
+
* @type {ButtonMenuConfig}
|
|
480
|
+
*/
|
|
481
|
+
ButtonMenu.defaults = Object.freeze({
|
|
482
|
+
buttonText: 'Actions',
|
|
483
|
+
alignMenu: 'left',
|
|
484
|
+
buttonClasses: ''
|
|
485
|
+
});
|
|
486
|
+
/**
|
|
487
|
+
* Button menu config schema
|
|
488
|
+
*
|
|
489
|
+
* @type {Schema<ButtonMenuConfig>}
|
|
490
|
+
*/
|
|
491
|
+
ButtonMenu.schema = Object.freeze(/** @type {const} */{
|
|
492
|
+
properties: {
|
|
493
|
+
buttonText: {
|
|
494
|
+
type: 'string'
|
|
495
|
+
},
|
|
496
|
+
buttonClasses: {
|
|
497
|
+
type: 'string'
|
|
498
|
+
},
|
|
499
|
+
alignMenu: {
|
|
500
|
+
type: 'string'
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
290
505
|
export { ButtonMenu };
|
|
291
506
|
//# sourceMappingURL=button-menu.bundle.mjs.map
|