@ministryofjustice/frontend 4.0.1 → 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.
Files changed (256) hide show
  1. package/govuk-prototype-kit.config.json +19 -4
  2. package/moj/_base.scss +2 -0
  3. package/moj/_base.scss.map +1 -0
  4. package/moj/all.bundle.js +3010 -0
  5. package/moj/all.bundle.js.map +1 -0
  6. package/moj/all.bundle.mjs +3293 -0
  7. package/moj/all.bundle.mjs.map +1 -0
  8. package/moj/all.mjs +17 -110
  9. package/moj/all.mjs.map +1 -1
  10. package/moj/all.scss +3 -0
  11. package/moj/all.scss.map +1 -0
  12. package/moj/common/index.mjs +57 -0
  13. package/moj/common/index.mjs.map +1 -0
  14. package/moj/common/moj-frontend-version.mjs +14 -0
  15. package/moj/common/moj-frontend-version.mjs.map +1 -0
  16. package/moj/components/_all.scss +2 -0
  17. package/moj/components/_all.scss.map +1 -0
  18. package/moj/components/action-bar/_action-bar.scss +2 -0
  19. package/moj/components/action-bar/_action-bar.scss.map +1 -0
  20. package/moj/components/add-another/_add-another.scss +2 -0
  21. package/moj/components/add-another/_add-another.scss.map +1 -0
  22. package/moj/components/add-another/add-another.bundle.js +157 -0
  23. package/moj/components/add-another/add-another.bundle.js.map +1 -0
  24. package/moj/components/add-another/add-another.bundle.mjs +271 -0
  25. package/moj/components/add-another/add-another.bundle.mjs.map +1 -0
  26. package/moj/components/add-another/add-another.mjs +135 -91
  27. package/moj/components/add-another/add-another.mjs.map +1 -1
  28. package/moj/components/alert/_alert.scss +4 -0
  29. package/moj/components/alert/_alert.scss.map +1 -0
  30. package/moj/components/alert/alert.bundle.js +254 -0
  31. package/moj/components/alert/alert.bundle.js.map +1 -0
  32. package/moj/components/alert/alert.bundle.mjs +490 -0
  33. package/moj/components/alert/alert.bundle.mjs.map +1 -0
  34. package/moj/components/alert/alert.mjs +97 -218
  35. package/moj/components/alert/alert.mjs.map +1 -1
  36. package/moj/components/alert/{alert.spec.helper.js → alert.spec.helper.bundle.js} +1 -1
  37. package/moj/components/alert/alert.spec.helper.bundle.js.map +1 -0
  38. package/moj/components/alert/alert.spec.helper.bundle.mjs +67 -0
  39. package/moj/components/alert/alert.spec.helper.bundle.mjs.map +1 -0
  40. package/moj/components/alert/alert.spec.helper.mjs.map +1 -1
  41. package/moj/components/badge/_badge.scss +2 -0
  42. package/moj/components/badge/_badge.scss.map +1 -0
  43. package/moj/components/banner/_banner.scss +2 -0
  44. package/moj/components/banner/_banner.scss.map +1 -0
  45. package/moj/components/button-menu/README.md +12 -6
  46. package/moj/components/button-menu/_button-menu.scss +4 -1
  47. package/moj/components/button-menu/_button-menu.scss.map +1 -0
  48. package/moj/components/button-menu/button-menu.bundle.js +270 -0
  49. package/moj/components/button-menu/button-menu.bundle.js.map +1 -0
  50. package/moj/components/button-menu/button-menu.bundle.mjs +506 -0
  51. package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -0
  52. package/moj/components/button-menu/button-menu.mjs +214 -280
  53. package/moj/components/button-menu/button-menu.mjs.map +1 -1
  54. package/moj/components/cookie-banner/_cookie-banner.scss +2 -0
  55. package/moj/components/cookie-banner/_cookie-banner.scss.map +1 -0
  56. package/moj/components/currency-input/_currency-input.scss +2 -0
  57. package/moj/components/currency-input/_currency-input.scss.map +1 -0
  58. package/moj/components/date-picker/_date-picker.scss +2 -0
  59. package/moj/components/date-picker/_date-picker.scss.map +1 -0
  60. package/moj/components/date-picker/date-picker.bundle.js +804 -0
  61. package/moj/components/date-picker/date-picker.bundle.js.map +1 -0
  62. package/moj/components/date-picker/date-picker.bundle.mjs +1040 -0
  63. package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -0
  64. package/moj/components/date-picker/date-picker.mjs +663 -827
  65. package/moj/components/date-picker/date-picker.mjs.map +1 -1
  66. package/moj/components/filter/_filter.scss +2 -0
  67. package/moj/components/filter/_filter.scss.map +1 -0
  68. package/moj/components/filter/template.njk +1 -1
  69. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +185 -0
  70. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -0
  71. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +421 -0
  72. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -0
  73. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +166 -81
  74. package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
  75. package/moj/components/form-validator/form-validator.bundle.js +288 -0
  76. package/moj/components/form-validator/form-validator.bundle.js.map +1 -0
  77. package/moj/components/form-validator/form-validator.bundle.mjs +524 -0
  78. package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -0
  79. package/moj/components/form-validator/form-validator.mjs +226 -149
  80. package/moj/components/form-validator/form-validator.mjs.map +1 -1
  81. package/moj/components/header/_header.scss +2 -0
  82. package/moj/components/header/_header.scss.map +1 -0
  83. package/moj/components/identity-bar/_identity-bar.scss +2 -0
  84. package/moj/components/identity-bar/_identity-bar.scss.map +1 -0
  85. package/moj/components/interruption-card/_interruption-card.scss +2 -0
  86. package/moj/components/interruption-card/_interruption-card.scss.map +1 -0
  87. package/moj/components/messages/_messages.scss +2 -0
  88. package/moj/components/messages/_messages.scss.map +1 -0
  89. package/moj/components/multi-file-upload/_multi-file-upload.scss +2 -0
  90. package/moj/components/multi-file-upload/_multi-file-upload.scss.map +1 -0
  91. package/moj/components/multi-file-upload/multi-file-upload.bundle.js +397 -0
  92. package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -0
  93. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +633 -0
  94. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -0
  95. package/moj/components/multi-file-upload/multi-file-upload.mjs +384 -213
  96. package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
  97. package/moj/components/multi-file-upload/template.njk +1 -1
  98. package/moj/components/multi-select/_multi-select.scss +2 -0
  99. package/moj/components/multi-select/_multi-select.scss.map +1 -0
  100. package/moj/components/multi-select/multi-select.bundle.js +143 -0
  101. package/moj/components/multi-select/multi-select.bundle.js.map +1 -0
  102. package/moj/components/multi-select/multi-select.bundle.mjs +379 -0
  103. package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -0
  104. package/moj/components/multi-select/multi-select.mjs +123 -64
  105. package/moj/components/multi-select/multi-select.mjs.map +1 -1
  106. package/moj/components/notification-badge/_notification-badge.scss +2 -0
  107. package/moj/components/notification-badge/_notification-badge.scss.map +1 -0
  108. package/moj/components/organisation-switcher/_organisation-switcher.scss +2 -0
  109. package/moj/components/organisation-switcher/_organisation-switcher.scss.map +1 -0
  110. package/moj/components/page-header-actions/_page-header-actions.scss +2 -0
  111. package/moj/components/page-header-actions/_page-header-actions.scss.map +1 -0
  112. package/moj/components/pagination/_pagination.scss +2 -2
  113. package/moj/components/pagination/_pagination.scss.map +1 -0
  114. package/moj/components/password-reveal/_password-reveal.scss +5 -1
  115. package/moj/components/password-reveal/_password-reveal.scss.map +1 -0
  116. package/moj/components/password-reveal/password-reveal.bundle.js +52 -0
  117. package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -0
  118. package/moj/components/password-reveal/password-reveal.bundle.mjs +166 -0
  119. package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -0
  120. package/moj/components/password-reveal/password-reveal.mjs +39 -29
  121. package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
  122. package/moj/components/primary-navigation/_primary-navigation.scss +2 -0
  123. package/moj/components/primary-navigation/_primary-navigation.scss.map +1 -0
  124. package/moj/components/progress-bar/_progress-bar.scss +2 -0
  125. package/moj/components/progress-bar/_progress-bar.scss.map +1 -0
  126. package/moj/components/rich-text-editor/README.md +16 -9
  127. package/moj/components/rich-text-editor/_rich-text-editor.scss +2 -0
  128. package/moj/components/rich-text-editor/_rich-text-editor.scss.map +1 -0
  129. package/moj/components/rich-text-editor/rich-text-editor.bundle.js +210 -0
  130. package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -0
  131. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +446 -0
  132. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -0
  133. package/moj/components/rich-text-editor/rich-text-editor.mjs +186 -140
  134. package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
  135. package/moj/components/search/_search.scss +2 -0
  136. package/moj/components/search/_search.scss.map +1 -0
  137. package/moj/components/search-toggle/{search-toggle.scss → _search-toggle.scss} +2 -0
  138. package/moj/components/search-toggle/_search-toggle.scss.map +1 -0
  139. package/moj/components/search-toggle/search-toggle.bundle.js +122 -0
  140. package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -0
  141. package/moj/components/search-toggle/search-toggle.bundle.mjs +358 -0
  142. package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -0
  143. package/moj/components/search-toggle/search-toggle.mjs +104 -43
  144. package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
  145. package/moj/components/side-navigation/_side-navigation.scss +2 -0
  146. package/moj/components/side-navigation/_side-navigation.scss.map +1 -0
  147. package/moj/components/sortable-table/_sortable-table.scss +2 -2
  148. package/moj/components/sortable-table/_sortable-table.scss.map +1 -0
  149. package/moj/components/sortable-table/sortable-table.bundle.js +202 -0
  150. package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -0
  151. package/moj/components/sortable-table/sortable-table.bundle.mjs +438 -0
  152. package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -0
  153. package/moj/components/sortable-table/sortable-table.mjs +179 -122
  154. package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
  155. package/moj/components/sub-navigation/_sub-navigation.scss +2 -0
  156. package/moj/components/sub-navigation/_sub-navigation.scss.map +1 -0
  157. package/moj/components/tag/_tag.scss +2 -0
  158. package/moj/components/tag/_tag.scss.map +1 -0
  159. package/moj/components/task-list/_task-list.scss +2 -0
  160. package/moj/components/task-list/_task-list.scss.map +1 -0
  161. package/moj/components/ticket-panel/_ticket-panel.scss +2 -0
  162. package/moj/components/ticket-panel/_ticket-panel.scss.map +1 -0
  163. package/moj/components/timeline/_timeline.scss +2 -0
  164. package/moj/components/timeline/_timeline.scss.map +1 -0
  165. package/moj/core/_all.scss +3 -0
  166. package/moj/core/_all.scss.map +1 -0
  167. package/moj/core/_moj-frontend-properties.scss +7 -0
  168. package/moj/core/_moj-frontend-properties.scss.map +1 -0
  169. package/moj/filters/all.js +44 -22
  170. package/moj/filters/prototype-kit-13-filters.js +4 -3
  171. package/moj/helpers/_all.scss +2 -0
  172. package/moj/helpers/_all.scss.map +1 -0
  173. package/moj/helpers/_hidden.scss +2 -0
  174. package/moj/helpers/_hidden.scss.map +1 -0
  175. package/moj/helpers/_links.scss +2 -0
  176. package/moj/helpers/_links.scss.map +1 -0
  177. package/moj/helpers.bundle.js +140 -0
  178. package/moj/helpers.bundle.js.map +1 -0
  179. package/moj/helpers.bundle.mjs +128 -0
  180. package/moj/helpers.bundle.mjs.map +1 -0
  181. package/moj/helpers.mjs +50 -77
  182. package/moj/helpers.mjs.map +1 -1
  183. package/moj/init.js +11 -2
  184. package/moj/moj-frontend.min.css +1 -1
  185. package/moj/moj-frontend.min.css.map +1 -1
  186. package/moj/moj-frontend.min.js +1 -1
  187. package/moj/moj-frontend.min.js.map +1 -1
  188. package/moj/objects/_all.scss +2 -0
  189. package/moj/objects/_all.scss.map +1 -0
  190. package/moj/objects/_button-group.scss +2 -0
  191. package/moj/objects/_button-group.scss.map +1 -0
  192. package/moj/objects/_filter-layout.scss +2 -0
  193. package/moj/objects/_filter-layout.scss.map +1 -0
  194. package/moj/objects/_scrollable-pane.scss +2 -0
  195. package/moj/objects/_scrollable-pane.scss.map +1 -0
  196. package/moj/objects/_width-container.scss +2 -0
  197. package/moj/objects/_width-container.scss.map +1 -0
  198. package/moj/settings/_all.scss +2 -0
  199. package/moj/settings/_all.scss.map +1 -0
  200. package/moj/settings/_assets.scss +2 -0
  201. package/moj/settings/_assets.scss.map +1 -0
  202. package/moj/settings/_colours.scss +2 -0
  203. package/moj/settings/_colours.scss.map +1 -0
  204. package/moj/settings/_measurements.scss +2 -0
  205. package/moj/settings/_measurements.scss.map +1 -0
  206. package/moj/settings/_typography.scss +2 -0
  207. package/moj/settings/_typography.scss.map +1 -0
  208. package/moj/template.njk +13 -0
  209. package/moj/utilities/_all.scss +2 -0
  210. package/moj/utilities/_all.scss.map +1 -0
  211. package/moj/utilities/_hidden.scss +2 -0
  212. package/moj/utilities/_hidden.scss.map +1 -0
  213. package/moj/utilities/_width-container.scss +2 -0
  214. package/moj/utilities/_width-container.scss.map +1 -0
  215. package/moj/vendor/govuk-frontend/_base.scss +2 -0
  216. package/moj/vendor/govuk-frontend/_base.scss.map +1 -0
  217. package/moj/vendor/govuk-frontend/_index.scss +2 -0
  218. package/moj/vendor/govuk-frontend/_index.scss.map +1 -0
  219. package/package.json +5 -6
  220. package/moj/all.jquery.min.js +0 -1
  221. package/moj/all.jquery.min.js.map +0 -1
  222. package/moj/all.js +0 -2662
  223. package/moj/all.js.map +0 -1
  224. package/moj/components/add-another/add-another.js +0 -115
  225. package/moj/components/add-another/add-another.js.map +0 -1
  226. package/moj/components/alert/alert.js +0 -356
  227. package/moj/components/alert/alert.js.map +0 -1
  228. package/moj/components/alert/alert.spec.helper.js.map +0 -1
  229. package/moj/components/button-menu/button-menu.js +0 -338
  230. package/moj/components/button-menu/button-menu.js.map +0 -1
  231. package/moj/components/date-picker/date-picker.js +0 -970
  232. package/moj/components/date-picker/date-picker.js.map +0 -1
  233. package/moj/components/filter-toggle-button/filter-toggle-button.js +0 -102
  234. package/moj/components/filter-toggle-button/filter-toggle-button.js.map +0 -1
  235. package/moj/components/form-validator/form-validator.js +0 -205
  236. package/moj/components/form-validator/form-validator.js.map +0 -1
  237. package/moj/components/multi-file-upload/multi-file-upload.js +0 -241
  238. package/moj/components/multi-file-upload/multi-file-upload.js.map +0 -1
  239. package/moj/components/multi-select/multi-select.js +0 -86
  240. package/moj/components/multi-select/multi-select.js.map +0 -1
  241. package/moj/components/password-reveal/password-reveal.js +0 -44
  242. package/moj/components/password-reveal/password-reveal.js.map +0 -1
  243. package/moj/components/rich-text-editor/rich-text-editor.js +0 -166
  244. package/moj/components/rich-text-editor/rich-text-editor.js.map +0 -1
  245. package/moj/components/search-toggle/search-toggle.js +0 -63
  246. package/moj/components/search-toggle/search-toggle.js.map +0 -1
  247. package/moj/components/sortable-table/sortable-table.js +0 -147
  248. package/moj/components/sortable-table/sortable-table.js.map +0 -1
  249. package/moj/helpers.js +0 -200
  250. package/moj/helpers.js.map +0 -1
  251. package/moj/vendor/html5shiv.js +0 -326
  252. package/moj/vendor/jquery.js +0 -9300
  253. package/moj/version.js +0 -12
  254. package/moj/version.js.map +0 -1
  255. package/moj/version.mjs +0 -4
  256. package/moj/version.mjs.map +0 -1
@@ -0,0 +1,1040 @@
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 {
37
+ /**
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
146
+ */
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'));
156
+ }
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);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ return newObject[namespace];
243
+ }
244
+
245
+ /**
246
+ * @augments {ConfigurableComponent<DatePickerConfig>}
247
+ */
248
+ class DatePicker extends ConfigurableComponent {
249
+ /**
250
+ * @param {Element | null} $root - HTML element to use for date picker
251
+ * @param {DatePickerConfig} [config] - Date picker config
252
+ */
253
+ constructor($root, config = {}) {
254
+ var _this$config$input$el;
255
+ super($root, config);
256
+ const $input = (_this$config$input$el = this.config.input.element) != null ? _this$config$input$el : this.$root.querySelector(this.config.input.selector);
257
+ if (!$input || !($input instanceof HTMLInputElement)) {
258
+ return this;
259
+ }
260
+ this.$input = $input;
261
+ this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
262
+ this.monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
263
+ this.currentDate = new Date();
264
+ this.currentDate.setHours(0, 0, 0, 0);
265
+ this.calendarDays = /** @type {DSCalendarDay[]} */[];
266
+ this.excludedDates = /** @type {Date[]} */[];
267
+ this.excludedDays = /** @type {number[]} */[];
268
+ this.buttonClass = 'moj-datepicker__button';
269
+ this.selectedDayButtonClass = 'moj-datepicker__button--selected';
270
+ this.currentDayButtonClass = 'moj-datepicker__button--current';
271
+ this.todayButtonClass = 'moj-datepicker__button--today';
272
+ this.setOptions();
273
+ this.initControls();
274
+ }
275
+ initControls() {
276
+ this.id = `datepicker-${this.$input.id}`;
277
+ this.$dialog = this.createDialog();
278
+ this.createCalendarHeaders();
279
+ const $componentWrapper = document.createElement('div');
280
+ const $inputWrapper = document.createElement('div');
281
+ $componentWrapper.classList.add('moj-datepicker__wrapper');
282
+ $inputWrapper.classList.add('govuk-input__wrapper');
283
+ this.$input.parentElement.insertBefore($componentWrapper, this.$input);
284
+ $componentWrapper.appendChild($inputWrapper);
285
+ $inputWrapper.appendChild(this.$input);
286
+ $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
287
+ $componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
288
+ this.$calendarButton = /** @type {HTMLButtonElement} */
289
+ this.$root.querySelector('.moj-js-datepicker-toggle');
290
+ this.$dialogTitle = /** @type {HTMLHeadingElement} */
291
+ this.$dialog.querySelector('.moj-js-datepicker-month-year');
292
+ this.createCalendar();
293
+ this.$prevMonthButton = /** @type {HTMLButtonElement} */
294
+ this.$dialog.querySelector('.moj-js-datepicker-prev-month');
295
+ this.$prevYearButton = /** @type {HTMLButtonElement} */
296
+ this.$dialog.querySelector('.moj-js-datepicker-prev-year');
297
+ this.$nextMonthButton = /** @type {HTMLButtonElement} */
298
+ this.$dialog.querySelector('.moj-js-datepicker-next-month');
299
+ this.$nextYearButton = /** @type {HTMLButtonElement} */
300
+ this.$dialog.querySelector('.moj-js-datepicker-next-year');
301
+ this.$cancelButton = /** @type {HTMLButtonElement} */
302
+ this.$dialog.querySelector('.moj-js-datepicker-cancel');
303
+ this.$okButton = /** @type {HTMLButtonElement} */
304
+ this.$dialog.querySelector('.moj-js-datepicker-ok');
305
+
306
+ // add event listeners
307
+ this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false));
308
+ this.$prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false));
309
+ this.$nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false));
310
+ this.$nextYearButton.addEventListener('click', event => this.focusNextYear(event, false));
311
+ this.$cancelButton.addEventListener('click', event => {
312
+ event.preventDefault();
313
+ this.closeDialog();
314
+ });
315
+ this.$okButton.addEventListener('click', () => {
316
+ this.selectDate(this.currentDate);
317
+ });
318
+ const $dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
319
+ this.$firstButtonInDialog = $dialogButtons[0];
320
+ this.$lastButtonInDialog = $dialogButtons[$dialogButtons.length - 1];
321
+ this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event));
322
+ this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event));
323
+ this.$calendarButton.addEventListener('click', event => this.toggleDialog(event));
324
+ this.$dialog.addEventListener('keydown', event => {
325
+ if (event.key === 'Escape') {
326
+ this.closeDialog();
327
+ event.preventDefault();
328
+ event.stopPropagation();
329
+ }
330
+ });
331
+ document.body.addEventListener('mouseup', event => this.backgroundClick(event));
332
+
333
+ // populates calendar with initial dates, avoids Wave errors about null buttons
334
+ this.updateCalendar();
335
+ }
336
+ createDialog() {
337
+ const titleId = `datepicker-title-${this.$input.id}`;
338
+ const $dialog = document.createElement('div');
339
+ $dialog.id = this.id;
340
+ $dialog.setAttribute('class', 'moj-datepicker__dialog');
341
+ $dialog.setAttribute('role', 'dialog');
342
+ $dialog.setAttribute('aria-modal', 'true');
343
+ $dialog.setAttribute('aria-labelledby', titleId);
344
+ $dialog.innerHTML = this.dialogTemplate(titleId);
345
+ $dialog.hidden = true;
346
+ return $dialog;
347
+ }
348
+ createCalendar() {
349
+ const $tbody = this.$dialog.querySelector('tbody');
350
+ let dayCount = 0;
351
+ for (let i = 0; i < 6; i++) {
352
+ // create row
353
+ const $row = $tbody.insertRow(i);
354
+ for (let j = 0; j < 7; j++) {
355
+ // create cell (day)
356
+ const $cell = document.createElement('td');
357
+ const $dateButton = document.createElement('button');
358
+ $cell.appendChild($dateButton);
359
+ $row.appendChild($cell);
360
+ const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this);
361
+ this.calendarDays.push(calendarDay);
362
+ dayCount++;
363
+ }
364
+ }
365
+ }
366
+ toggleTemplate() {
367
+ return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
368
+ <span class="govuk-visually-hidden">Choose date</span>
369
+ <svg width="32" height="24" focusable="false" class="moj-datepicker-icon" aria-hidden="true" role="img" viewBox="0 0 22 22">
370
+ <path
371
+ fill="currentColor"
372
+ fill-rule="evenodd"
373
+ clip-rule="evenodd"
374
+ d="M16.1333 2.93333H5.86668V4.4C5.86668 5.21002 5.21003 5.86667 4.40002 5.86667C3.59 5.86667 2.93335 5.21002 2.93335 4.4V2.93333H2C0.895431 2.93333 0 3.82877 0 4.93334V19.2667C0 20.3712 0.89543 21.2667 2 21.2667H20C21.1046 21.2667 22 20.3712 22 19.2667V4.93333C22 3.82876 21.1046 2.93333 20 2.93333H19.0667V4.4C19.0667 5.21002 18.41 5.86667 17.6 5.86667C16.79 5.86667 16.1333 5.21002 16.1333 4.4V2.93333ZM20.5333 8.06667H1.46665V18.8C1.46665 19.3523 1.91436 19.8 2.46665 19.8H19.5333C20.0856 19.8 20.5333 19.3523 20.5333 18.8V8.06667Z"
375
+ ></path>
376
+ <rect x="3.66669" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
377
+ <rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
378
+ </svg>
379
+ </button>`;
380
+ }
381
+
382
+ /**
383
+ * HTML template for calendar dialog
384
+ *
385
+ * @param {string} [titleId] - Id attribute for dialog title
386
+ * @returns {string}
387
+ */
388
+ dialogTemplate(titleId) {
389
+ return `<div class="moj-datepicker__dialog-header">
390
+ <div class="moj-datepicker__dialog-navbuttons">
391
+ <button class="moj-datepicker__button moj-js-datepicker-prev-year">
392
+ <span class="govuk-visually-hidden">Previous year</span>
393
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" fill="none" focusable="false" aria-hidden="true" role="img">
394
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M23.1643 20L28.9572 14.2071L27.5429 12.7929L20.3358 20L27.5429 27.2071L28.9572 25.7929L23.1643 20Z" fill="currentColor"/>
395
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M17.1643 20L22.9572 14.2071L21.5429 12.7929L14.3358 20L21.5429 27.2071L22.9572 25.7929L17.1643 20Z" fill="currentColor"/>
396
+ </svg>
397
+ </button>
398
+
399
+ <button class="moj-datepicker__button moj-js-datepicker-prev-month">
400
+ <span class="govuk-visually-hidden">Previous month</span>
401
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" focusable="false" aria-hidden="true" role="img">
402
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M20.5729 20L25.7865 14.2071L24.5137 12.7929L18.0273 20L24.5137 27.2071L25.7865 25.7929L20.5729 20Z" fill="currentColor"/>
403
+ </svg>
404
+ </button>
405
+ </div>
406
+
407
+ <h2 id="${titleId}" class="moj-datepicker__dialog-title moj-js-datepicker-month-year" aria-live="polite">June 2020</h2>
408
+
409
+ <div class="moj-datepicker__dialog-navbuttons">
410
+ <button class="moj-datepicker__button moj-js-datepicker-next-month">
411
+ <span class="govuk-visually-hidden">Next month</span>
412
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" focusable="false" aria-hidden="true" role="img">
413
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M23.4271 20L18.2135 14.2071L19.4863 12.7929L25.9727 20L19.4863 27.2071L18.2135 25.7929L23.4271 20Z" fill="currentColor"/>
414
+ </svg>
415
+ </button>
416
+
417
+ <button class="moj-datepicker__button moj-js-datepicker-next-year">
418
+ <span class="govuk-visually-hidden">Next year</span>
419
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" fill="none" focusable="false" aria-hidden="true" role="img">
420
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M20.8357 20L15.0428 14.2071L16.4571 12.7929L23.6642 20L16.4571 27.2071L15.0428 25.7929L20.8357 20Z" fill="currentColor"/>
421
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M26.8357 20L21.0428 14.2071L22.4571 12.7929L29.6642 20L22.4571 27.2071L21.0428 25.7929L26.8357 20Z" fill="currentColor"/>
422
+ </svg>
423
+ </button>
424
+ </div>
425
+ </div>
426
+
427
+ <table class="moj-datepicker__calendar moj-js-datepicker-grid" role="grid" aria-labelledby="${titleId}">
428
+ <thead>
429
+ <tr></tr>
430
+ </thead>
431
+
432
+ <tbody></tbody>
433
+ </table>
434
+
435
+ <div class="govuk-button-group">
436
+ <button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
437
+ <button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
438
+ </div>`;
439
+ }
440
+ createCalendarHeaders() {
441
+ this.dayLabels.forEach(day => {
442
+ const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th>`;
443
+ const $headerRow = this.$dialog.querySelector('thead > tr');
444
+ $headerRow.insertAdjacentHTML('beforeend', html);
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Pads given number with leading zeros
450
+ *
451
+ * @param {number} value - The value to be padded
452
+ * @param {number} length - The length in characters of the output
453
+ * @returns {string}
454
+ */
455
+ leadingZeros(value, length = 2) {
456
+ let ret = value.toString();
457
+ while (ret.length < length) {
458
+ ret = `0${ret}`;
459
+ }
460
+ return ret;
461
+ }
462
+ setOptions() {
463
+ this.setMinAndMaxDatesOnCalendar();
464
+ this.setExcludedDates();
465
+ this.setExcludedDays();
466
+ this.setWeekStartDay();
467
+ }
468
+ setMinAndMaxDatesOnCalendar() {
469
+ if (this.config.minDate) {
470
+ this.minDate = this.formattedDateFromString(this.config.minDate, null);
471
+ if (this.minDate && this.currentDate < this.minDate) {
472
+ this.currentDate = this.minDate;
473
+ }
474
+ }
475
+ if (this.config.maxDate) {
476
+ this.maxDate = this.formattedDateFromString(this.config.maxDate, null);
477
+ if (this.maxDate && this.currentDate > this.maxDate) {
478
+ this.currentDate = this.maxDate;
479
+ }
480
+ }
481
+ }
482
+ setExcludedDates() {
483
+ if (this.config.excludedDates) {
484
+ this.excludedDates = this.config.excludedDates.replace(/\s+/, ' ').split(' ').map(item => {
485
+ return item.includes('-') ? this.parseDateRangeString(item) : [this.formattedDateFromString(item)];
486
+ }).reduce((dates, items) => dates.concat(items)).filter(date => date);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Parses a daterange string into an array of dates
492
+ *
493
+ * @param {string} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
494
+ */
495
+ parseDateRangeString(datestring) {
496
+ const dates = [];
497
+ const [startDate, endDate] = datestring.split('-').map(d => this.formattedDateFromString(d, null));
498
+ if (startDate && endDate) {
499
+ const date = new Date(startDate.getTime());
500
+ /* eslint-disable no-unmodified-loop-condition */
501
+ while (date <= endDate) {
502
+ dates.push(new Date(date));
503
+ date.setDate(date.getDate() + 1);
504
+ }
505
+ /* eslint-enable no-unmodified-loop-condition */
506
+ }
507
+ return dates;
508
+ }
509
+ setExcludedDays() {
510
+ if (this.config.excludedDays) {
511
+ // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
512
+ // with getDay() function
513
+ const weekDays = this.dayLabels.map(item => item.toLowerCase());
514
+ if (this.config.weekStartDay === 'monday') {
515
+ weekDays.unshift(weekDays.pop());
516
+ }
517
+ this.excludedDays = this.config.excludedDays.replace(/\s+/, ' ').toLowerCase().split(' ').map(item => weekDays.indexOf(item)).filter(item => item !== -1);
518
+ }
519
+ }
520
+ setWeekStartDay() {
521
+ const weekStartDayParam = this.config.weekStartDay;
522
+ if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
523
+ this.config.weekStartDay = 'sunday';
524
+ // Rotate dayLabels array to put Sunday as the first item
525
+ this.dayLabels.unshift(this.dayLabels.pop());
526
+ } else {
527
+ this.config.weekStartDay = 'monday';
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Determine if a date is selectable
533
+ *
534
+ * @param {Date} date - the date to check
535
+ * @returns {boolean}
536
+ */
537
+ isExcludedDate(date) {
538
+ // This comparison does not work correctly - it will exclude the mindate itself
539
+ // see: https://github.com/ministryofjustice/moj-frontend/issues/923
540
+ if (this.minDate && this.minDate > date) {
541
+ return true;
542
+ }
543
+
544
+ // This comparison works as expected - the maxdate will not be excluded
545
+ if (this.maxDate && this.maxDate < date) {
546
+ return true;
547
+ }
548
+ for (const excludedDate of this.excludedDates) {
549
+ if (date.toDateString() === excludedDate.toDateString()) {
550
+ return true;
551
+ }
552
+ }
553
+ if (this.excludedDays.includes(date.getDay())) {
554
+ return true;
555
+ }
556
+ return false;
557
+ }
558
+
559
+ /**
560
+ * Get a Date object from a string
561
+ *
562
+ * @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
563
+ * @param {Date} fallback - date object to return if formatting fails
564
+ * @returns {Date}
565
+ */
566
+ formattedDateFromString(dateString, fallback = new Date()) {
567
+ let formattedDate = null;
568
+ // Accepts d/m/yyyy and dd/mm/yyyy
569
+ const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/;
570
+ if (!dateFormatPattern.test(dateString)) return fallback;
571
+ const match = dateFormatPattern.exec(dateString);
572
+ const day = match[1];
573
+ const month = match[3];
574
+ const year = match[4];
575
+ formattedDate = new Date(`${year}-${month}-${day}`);
576
+ if (formattedDate instanceof Date && Number.isFinite(formattedDate.getTime())) {
577
+ return formattedDate;
578
+ }
579
+ return fallback;
580
+ }
581
+
582
+ /**
583
+ * Get a formatted date string from a Date object
584
+ *
585
+ * @param {Date} date - date to format to a string
586
+ * @returns {string}
587
+ */
588
+ formattedDateFromDate(date) {
589
+ if (this.config.leadingZeros) {
590
+ return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`;
591
+ }
592
+ return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
593
+ }
594
+
595
+ /**
596
+ * Get a human readable date in the format Monday 2 March 2024
597
+ *
598
+ * @param {Date} date - Date to format
599
+ * @returns {string}
600
+ */
601
+ formattedDateHuman(date) {
602
+ return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`;
603
+ }
604
+
605
+ /**
606
+ * @param {MouseEvent} event - Click event
607
+ */
608
+ backgroundClick(event) {
609
+ if (this.isOpen() && event.target instanceof Node && !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target)) {
610
+ event.preventDefault();
611
+ this.closeDialog();
612
+ }
613
+ }
614
+
615
+ /**
616
+ * @param {KeyboardEvent} event - Keydown event
617
+ */
618
+ firstButtonKeydown(event) {
619
+ if (event.key === 'Tab' && event.shiftKey) {
620
+ this.$lastButtonInDialog.focus();
621
+ event.preventDefault();
622
+ }
623
+ }
624
+
625
+ /**
626
+ * @param {KeyboardEvent} event - Keydown event
627
+ */
628
+ lastButtonKeydown(event) {
629
+ if (event.key === 'Tab' && !event.shiftKey) {
630
+ this.$firstButtonInDialog.focus();
631
+ event.preventDefault();
632
+ }
633
+ }
634
+
635
+ // render calendar
636
+ updateCalendar() {
637
+ this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
638
+ const day = this.currentDate;
639
+ const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1);
640
+ let dayOfWeek;
641
+ if (this.config.weekStartDay === 'monday') {
642
+ dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0
643
+ } else {
644
+ dayOfWeek = firstOfMonth.getDay();
645
+ }
646
+ firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek);
647
+ const thisDay = new Date(firstOfMonth);
648
+
649
+ // loop through our days
650
+ for (const calendarDay of this.calendarDays) {
651
+ const hidden = thisDay.getMonth() !== day.getMonth();
652
+ const disabled = this.isExcludedDate(thisDay);
653
+ calendarDay.update(thisDay, hidden, disabled);
654
+ thisDay.setDate(thisDay.getDate() + 1);
655
+ }
656
+ }
657
+
658
+ /**
659
+ * @param {boolean} [focus] - Focus the day button
660
+ */
661
+ setCurrentDate(focus = true) {
662
+ const {
663
+ currentDate
664
+ } = this;
665
+ this.calendarDays.forEach(calendarDay => {
666
+ calendarDay.$button.classList.add('moj-datepicker__button');
667
+ calendarDay.$button.classList.add('moj-datepicker__calendar-day');
668
+ calendarDay.$button.setAttribute('tabindex', '-1');
669
+ calendarDay.$button.classList.remove(this.selectedDayButtonClass);
670
+ const calendarDayDate = calendarDay.date;
671
+ calendarDayDate.setHours(0, 0, 0, 0);
672
+ const today = new Date();
673
+ today.setHours(0, 0, 0, 0);
674
+ if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) {
675
+ if (focus) {
676
+ calendarDay.$button.setAttribute('tabindex', '0');
677
+ calendarDay.$button.focus();
678
+ calendarDay.$button.classList.add(this.selectedDayButtonClass);
679
+ }
680
+ }
681
+ if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) {
682
+ calendarDay.$button.classList.add(this.currentDayButtonClass);
683
+ calendarDay.$button.setAttribute('aria-current', 'date');
684
+ } else {
685
+ calendarDay.$button.classList.remove(this.currentDayButtonClass);
686
+ calendarDay.$button.removeAttribute('aria-current');
687
+ }
688
+ if (calendarDayDate.getTime() === today.getTime()) {
689
+ calendarDay.$button.classList.add(this.todayButtonClass);
690
+ } else {
691
+ calendarDay.$button.classList.remove(this.todayButtonClass);
692
+ }
693
+ });
694
+
695
+ // if no date is tab-able, make the first non-disabled date tab-able
696
+ if (!focus) {
697
+ const enabledDays = this.calendarDays.filter(calendarDay => {
698
+ return window.getComputedStyle(calendarDay.$button).display === 'block' && !calendarDay.$button.disabled;
699
+ });
700
+ enabledDays[0].$button.setAttribute('tabindex', '0');
701
+ this.currentDate = enabledDays[0].date;
702
+ }
703
+ }
704
+
705
+ /**
706
+ * @param {Date} date - Date to select
707
+ */
708
+ selectDate(date) {
709
+ if (this.isExcludedDate(date)) {
710
+ return;
711
+ }
712
+ this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman(date)}`;
713
+ this.$input.value = this.formattedDateFromDate(date);
714
+ const changeEvent = new Event('change', {
715
+ bubbles: true,
716
+ cancelable: true
717
+ });
718
+ this.$input.dispatchEvent(changeEvent);
719
+ this.closeDialog();
720
+ }
721
+ isOpen() {
722
+ return this.$dialog.classList.contains('moj-datepicker__dialog--open');
723
+ }
724
+
725
+ /**
726
+ * @param {MouseEvent} event - Click event
727
+ */
728
+ toggleDialog(event) {
729
+ event.preventDefault();
730
+ if (this.isOpen()) {
731
+ this.closeDialog();
732
+ } else {
733
+ this.setMinAndMaxDatesOnCalendar();
734
+ this.openDialog();
735
+ }
736
+ }
737
+ openDialog() {
738
+ this.$dialog.hidden = false;
739
+ this.$dialog.classList.add('moj-datepicker__dialog--open');
740
+ this.$calendarButton.setAttribute('aria-expanded', 'true');
741
+
742
+ // position the dialog
743
+ // if input is wider than dialog pin it to the right
744
+ if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
745
+ this.$dialog.style.right = `0px`;
746
+ }
747
+ this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`;
748
+
749
+ // get the date from the input element
750
+ this.inputDate = this.formattedDateFromString(this.$input.value);
751
+ this.currentDate = this.inputDate;
752
+ this.currentDate.setHours(0, 0, 0, 0);
753
+ this.updateCalendar();
754
+ this.setCurrentDate();
755
+ }
756
+ closeDialog() {
757
+ this.$dialog.hidden = true;
758
+ this.$dialog.classList.remove('moj-datepicker__dialog--open');
759
+ this.$calendarButton.setAttribute('aria-expanded', 'false');
760
+ this.$calendarButton.focus();
761
+ }
762
+
763
+ /**
764
+ * @param {Date} date - Date to go to
765
+ * @param {boolean} [focus] - Focus the day button
766
+ */
767
+ goToDate(date, focus) {
768
+ const current = this.currentDate;
769
+ this.currentDate = date;
770
+ if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) {
771
+ this.updateCalendar();
772
+ }
773
+ this.setCurrentDate(focus);
774
+ }
775
+
776
+ // day navigation
777
+ focusNextDay() {
778
+ const date = new Date(this.currentDate);
779
+ date.setDate(date.getDate() + 1);
780
+ this.goToDate(date);
781
+ }
782
+ focusPreviousDay() {
783
+ const date = new Date(this.currentDate);
784
+ date.setDate(date.getDate() - 1);
785
+ this.goToDate(date);
786
+ }
787
+
788
+ // week navigation
789
+ focusNextWeek() {
790
+ const date = new Date(this.currentDate);
791
+ date.setDate(date.getDate() + 7);
792
+ this.goToDate(date);
793
+ }
794
+ focusPreviousWeek() {
795
+ const date = new Date(this.currentDate);
796
+ date.setDate(date.getDate() - 7);
797
+ this.goToDate(date);
798
+ }
799
+ focusFirstDayOfWeek() {
800
+ const date = new Date(this.currentDate);
801
+ const firstDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 0 : 1;
802
+ const dayOfWeek = date.getDay();
803
+ const diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek;
804
+ date.setDate(date.getDate() - diff);
805
+ date.setHours(0, 0, 0, 0);
806
+ this.goToDate(date);
807
+ }
808
+ focusLastDayOfWeek() {
809
+ const date = new Date(this.currentDate);
810
+ const lastDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 6 : 0;
811
+ const dayOfWeek = date.getDay();
812
+ const diff = dayOfWeek <= lastDayOfWeekIndex ? lastDayOfWeekIndex - dayOfWeek : 7 - dayOfWeek;
813
+ date.setDate(date.getDate() + diff);
814
+ date.setHours(0, 0, 0, 0);
815
+ this.goToDate(date);
816
+ }
817
+
818
+ /**
819
+ * Month navigation
820
+ *
821
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
822
+ * @param {boolean} [focus] - Focus the day button
823
+ */
824
+ focusNextMonth(event, focus = true) {
825
+ event.preventDefault();
826
+ const date = new Date(this.currentDate);
827
+ date.setMonth(date.getMonth() + 1, 1);
828
+ this.goToDate(date, focus);
829
+ }
830
+
831
+ /**
832
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
833
+ * @param {boolean} [focus] - Focus the day button
834
+ */
835
+ focusPreviousMonth(event, focus = true) {
836
+ event.preventDefault();
837
+ const date = new Date(this.currentDate);
838
+ date.setMonth(date.getMonth() - 1, 1);
839
+ this.goToDate(date, focus);
840
+ }
841
+
842
+ /**
843
+ * Year navigation
844
+ *
845
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
846
+ * @param {boolean} [focus] - Focus the day button
847
+ */
848
+ focusNextYear(event, focus = true) {
849
+ event.preventDefault();
850
+ const date = new Date(this.currentDate);
851
+ date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
852
+ this.goToDate(date, focus);
853
+ }
854
+
855
+ /**
856
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
857
+ * @param {boolean} [focus] - Focus the day button
858
+ */
859
+ focusPreviousYear(event, focus = true) {
860
+ event.preventDefault();
861
+ const date = new Date(this.currentDate);
862
+ date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1);
863
+ this.goToDate(date, focus);
864
+ }
865
+
866
+ /**
867
+ * Name for the component used when initialising using data-module attributes.
868
+ */
869
+ }
870
+ DatePicker.moduleName = 'moj-date-picker';
871
+ /**
872
+ * Date picker default config
873
+ *
874
+ * @type {DatePickerConfig}
875
+ */
876
+ DatePicker.defaults = Object.freeze({
877
+ leadingZeros: false,
878
+ weekStartDay: 'monday',
879
+ input: {
880
+ selector: '.moj-js-datepicker-input'
881
+ }
882
+ });
883
+ /**
884
+ * Date picker config schema
885
+ *
886
+ * @satisfies {Schema<DatePickerConfig>}
887
+ */
888
+ DatePicker.schema = Object.freeze(/** @type {const} */{
889
+ properties: {
890
+ excludedDates: {
891
+ type: 'string'
892
+ },
893
+ excludedDays: {
894
+ type: 'string'
895
+ },
896
+ leadingZeros: {
897
+ type: 'boolean'
898
+ },
899
+ maxDate: {
900
+ type: 'string'
901
+ },
902
+ minDate: {
903
+ type: 'string'
904
+ },
905
+ weekStartDay: {
906
+ type: 'string'
907
+ },
908
+ input: {
909
+ type: 'object'
910
+ }
911
+ }
912
+ });
913
+ class DSCalendarDay {
914
+ /**
915
+ *
916
+ * @param {HTMLButtonElement} $button
917
+ * @param {number} index
918
+ * @param {number} row
919
+ * @param {number} column
920
+ * @param {DatePicker} picker
921
+ */
922
+ constructor($button, index, row, column, picker) {
923
+ this.index = index;
924
+ this.row = row;
925
+ this.column = column;
926
+ this.$button = $button;
927
+ this.picker = picker;
928
+ this.date = new Date();
929
+ this.$button.addEventListener('keydown', this.keyPress.bind(this));
930
+ this.$button.addEventListener('click', this.click.bind(this));
931
+ }
932
+
933
+ /**
934
+ * @param {Date} day - the Date for the calendar day
935
+ * @param {boolean} hidden - visibility of the day
936
+ * @param {boolean} disabled - is the day selectable or excluded
937
+ */
938
+ update(day, hidden, disabled) {
939
+ const label = day.getDate();
940
+ let accessibleLabel = this.picker.formattedDateHuman(day);
941
+ if (disabled) {
942
+ this.$button.setAttribute('aria-disabled', 'true');
943
+ accessibleLabel = `Excluded date, ${accessibleLabel}`;
944
+ } else {
945
+ this.$button.removeAttribute('aria-disabled');
946
+ }
947
+ if (hidden) {
948
+ this.$button.style.display = 'none';
949
+ } else {
950
+ this.$button.style.display = 'block';
951
+ }
952
+ this.$button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
953
+ this.$button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
954
+ this.date = new Date(day);
955
+ }
956
+
957
+ /**
958
+ * @param {MouseEvent} event - Click event
959
+ */
960
+ click(event) {
961
+ this.picker.goToDate(this.date);
962
+ this.picker.selectDate(this.date);
963
+ event.stopPropagation();
964
+ event.preventDefault();
965
+ }
966
+
967
+ /**
968
+ * @param {KeyboardEvent} event - Keydown event
969
+ */
970
+ keyPress(event) {
971
+ let calendarNavKey = true;
972
+ switch (event.key) {
973
+ case 'ArrowLeft':
974
+ this.picker.focusPreviousDay();
975
+ break;
976
+ case 'ArrowRight':
977
+ this.picker.focusNextDay();
978
+ break;
979
+ case 'ArrowUp':
980
+ this.picker.focusPreviousWeek();
981
+ break;
982
+ case 'ArrowDown':
983
+ this.picker.focusNextWeek();
984
+ break;
985
+ case 'Home':
986
+ this.picker.focusFirstDayOfWeek();
987
+ break;
988
+ case 'End':
989
+ this.picker.focusLastDayOfWeek();
990
+ break;
991
+ case 'PageUp':
992
+ {
993
+ if (event.shiftKey) {
994
+ this.picker.focusPreviousYear(event);
995
+ } else {
996
+ this.picker.focusPreviousMonth(event);
997
+ }
998
+ break;
999
+ }
1000
+ case 'PageDown':
1001
+ {
1002
+ if (event.shiftKey) {
1003
+ this.picker.focusNextYear(event);
1004
+ } else {
1005
+ this.picker.focusNextMonth(event);
1006
+ }
1007
+ break;
1008
+ }
1009
+ default:
1010
+ calendarNavKey = false;
1011
+ break;
1012
+ }
1013
+ if (calendarNavKey) {
1014
+ event.preventDefault();
1015
+ event.stopPropagation();
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ /**
1021
+ * Date picker config
1022
+ *
1023
+ * @typedef {object} DatePickerConfig
1024
+ * @property {string} [excludedDates] - Dates that cannot be selected
1025
+ * @property {string} [excludedDays] - Days that cannot be selected
1026
+ * @property {boolean} [leadingZeros] - Whether to add leading zeroes when populating the field
1027
+ * @property {string} [minDate] - The earliest available date
1028
+ * @property {string} [maxDate] - The latest available date
1029
+ * @property {string} [weekStartDay] - First day of the week in calendar view
1030
+ * @property {object} [input] - Input config
1031
+ * @property {string} [input.selector] - Selector for the input element
1032
+ * @property {Element | null} [input.element] - HTML element for the input
1033
+ */
1034
+
1035
+ /**
1036
+ * @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
1037
+ */
1038
+
1039
+ export { DatePicker };
1040
+ //# sourceMappingURL=date-picker.bundle.mjs.map