-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Switch to nested (not flattened) configs with stricter checks #4792
Conversation
📋 StatsFile sizes
Modules
View stats and visualisations on the review app Action run for 8ce1dc4 |
JavaScript changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 32ea76aa4..4fa94bfc7 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1,108 +1,119 @@
const version = "development";
-function mergeConfigs(...t) {
- function flattenObject(t) {
- const e = {};
- return function flattenLoop(t, n) {
- for (const [i, s] of Object.entries(t)) {
- const t = n ? `${n}.${i}` : i;
- s && "object" == typeof s ? flattenLoop(s, t) : e[t] = s
- }
- }(t), e
- }
- const e = {};
- for (const n of t) {
- const t = flattenObject(n);
- for (const [n, i] of Object.entries(t)) e[n] = i
- }
- return e
+function normaliseString(e, t) {
+ const n = e ? e.trim() : "";
+ let i, s = null == t ? void 0 : t.type;
+ switch (s || (["true", "false"].includes(n) && (s = "boolean"), n.length > 0 && isFinite(Number(n)) && (s = "number")), s) {
+ case "boolean":
+ i = "true" === n;
+ break;
+ case "number":
+ i = Number(n);
+ break;
+ default:
+ i = e
+ }
+ return i
}
-function extractConfigByNamespace(t, e) {
- const n = {};
- for (const [i, s] of Object.entries(t)) {
- const t = i.split(".");
- if (t[0] === e) {
- t.length > 1 && t.shift();
- n[t.join(".")] = s
+function mergeConfigs(...e) {
+ const t = {};
+ for (const n of e)
+ for (const e of Object.keys(n)) {
+ const i = t[e],
+ s = n[e];
+ isObject(i) && isObject(s) ? t[e] = mergeConfigs(i, s) : t[e] = s
}
- }
- return n
+ return t
+}
+
+function extractConfigByNamespace(e, t, n) {
+ const i = e.schema.properties[n];
+ if ("object" !== (null == i ? void 0 : i.type)) return;
+ const s = {
+ [n]: {}
+ };
+ for (const [o, r] of Object.entries(t)) {
+ let e = s;
+ const t = o.split(".");
+ for (const [i, s] of t.entries()) "object" == typeof e && (i < t.length - 1 ? (isObject(e[s]) || (e[s] = {}), e = e[s]) : o !== n && (e[s] = normaliseString(r)))
+ }
+ return s[n]
}
-function getFragmentFromUrl(t) {
- if (t.includes("#")) return t.split("#").pop()
+function getFragmentFromUrl(e) {
+ if (e.includes("#")) return e.split("#").pop()
}
-function getBreakpoint(t) {
- const e = `--govuk-frontend-breakpoint-${t}`;
+function getBreakpoint(e) {
+ const t = `--govuk-frontend-breakpoint-${e}`;
return {
- property: e,
- value: window.getComputedStyle(document.documentElement).getPropertyValue(e) || void 0
+ property: t,
+ value: window.getComputedStyle(document.documentElement).getPropertyValue(t) || void 0
}
}
-function setFocus(t, e = {}) {
+function setFocus(e, t = {}) {
var n;
- const i = t.getAttribute("tabindex");
+ const i = e.getAttribute("tabindex");
function onBlur() {
var n;
- null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
+ null == (n = t.onBlur) || n.call(e), i || e.removeAttribute("tabindex")
}
- i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
- t.addEventListener("blur", onBlur, {
+ i || e.setAttribute("tabindex", "-1"), e.addEventListener("focus", (function() {
+ e.addEventListener("blur", onBlur, {
once: !0
})
}), {
once: !0
- }), null == (n = e.onBeforeFocus) || n.call(t), t.focus()
+ }), null == (n = t.onBeforeFocus) || n.call(e), e.focus()
}
-function isSupported(t = document.body) {
- return !!t && t.classList.contains("govuk-frontend-supported")
+function isSupported(e = document.body) {
+ return !!e && e.classList.contains("govuk-frontend-supported")
}
-function normaliseString(t) {
- if ("string" != typeof t) return t;
- const e = t.trim();
- return "true" === e || "false" !== e && (e.length > 0 && isFinite(Number(e)) ? Number(e) : t)
+function isObject(e) {
+ return !!e && "object" == typeof e && ! function(e) {
+ return Array.isArray(e)
+ }(e)
}
-function normaliseDataset(t) {
- const e = {};
- for (const [n, i] of Object.entries(t)) e[n] = normaliseString(i);
- return e
+function normaliseDataset(e, t) {
+ const n = {};
+ for (const [i, s] of Object.entries(e.schema.properties)) i in t && (n[i] = normaliseString(t[i], s)), "object" === (null == s ? void 0 : s.type) && (n[i] = extractConfigByNamespace(e, t, i));
+ return n
}
class GOVUKFrontendError extends Error {
- constructor(...t) {
- super(...t), this.name = "GOVUKFrontendError"
+ constructor(...e) {
+ super(...e), this.name = "GOVUKFrontendError"
}
}
class SupportError extends GOVUKFrontendError {
- constructor(t = document.body) {
- const e = "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";
- super(t ? e : 'GOV.UK Frontend initialised without `<script type="module">`'), this.name = "SupportError"
+ constructor(e = document.body) {
+ const t = "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";
+ super(e ? t : 'GOV.UK Frontend initialised without `<script type="module">`'), this.name = "SupportError"
}
}
class ConfigError extends GOVUKFrontendError {
- constructor(...t) {
- super(...t), this.name = "ConfigError"
+ constructor(...e) {
+ super(...e), this.name = "ConfigError"
}
}
class ElementError extends GOVUKFrontendError {
- constructor(t) {
- let e = "string" == typeof t ? t : "";
- if ("object" == typeof t) {
+ constructor(e) {
+ let t = "string" == typeof e ? e : "";
+ if ("object" == typeof e) {
const {
componentName: n,
identifier: i,
element: s,
expectedType: o
- } = t;
- e = `${n}: ${i}`, e += s ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
+ } = e;
+ t = `${n}: ${i}`, t += s ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
}
- super(e), this.name = "ElementError"
+ super(t), this.name = "ElementError"
}
}
class GOVUKFrontendComponent {
@@ -114,53 +125,59 @@ class GOVUKFrontendComponent {
}
}
class I18n {
- constructor(t = {}, e = {}) {
+ constructor(e = {}, t = {}) {
var n;
- this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (n = e.locale) ? n : document.documentElement.lang || "en"
- }
- t(t, e) {
- if (!t) throw new Error("i18n: lookup key missing");
- "number" == typeof(null == e ? void 0 : e.count) && (t = `${t}.${this.getPluralSuffix(t,e.count)}`);
- const n = this.translations[t];
+ this.translations = void 0, this.locale = void 0, this.translations = e, this.locale = null != (n = t.locale) ? n : document.documentElement.lang || "en"
+ }
+ t(e, t) {
+ if (!e) throw new Error("i18n: lookup key missing");
+ let n = this.translations[e];
+ if ("number" == typeof(null == t ? void 0 : t.count) && "object" == typeof n) {
+ const i = n[this.getPluralSuffix(e, t.count)];
+ i && (n = i)
+ }
if ("string" == typeof n) {
if (n.match(/%{(.\S+)}/)) {
- if (!e) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
- return this.replacePlaceholders(n, e)
+ if (!t) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
+ return this.replacePlaceholders(n, t)
}
return n
}
- return t
+ return e
}
- replacePlaceholders(t, e) {
+ replacePlaceholders(e, t) {
const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
- return t.replace(/%{(.\S+)}/g, (function(t, i) {
- if (Object.prototype.hasOwnProperty.call(e, i)) {
- const t = e[i];
- return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? n ? n.format(t) : `${t}` : t
+ return e.replace(/%{(.\S+)}/g, (function(e, i) {
+ if (Object.prototype.hasOwnProperty.call(t, i)) {
+ const e = t[i];
+ return !1 === e || "number" != typeof e && "string" != typeof e ? "" : "number" == typeof e ? n ? n.format(e) : `${e}` : e
}
- throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
+ throw new Error(`i18n: no data found to replace ${e} placeholder in string`)
}))
}
hasIntlPluralRulesSupport() {
return Boolean("PluralRules" in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length)
}
- getPluralSuffix(t, e) {
- if (e = Number(e), !isFinite(e)) return "other";
- const n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
- if (`${t}.${n}` in this.translations) return n;
- if (`${t}.other` in this.translations) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other";
+ getPluralSuffix(e, t) {
+ if (t = Number(t), !isFinite(t)) return "other";
+ const n = this.translations[e],
+ i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(t) : this.selectPluralFormUsingFallbackRules(t);
+ if ("object" == typeof n) {
+ if (i in n) return i;
+ if ("other" in n) return console.warn(`i18n: Missing plural form ".${i}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+ }
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
}
- selectPluralFormUsingFallbackRules(t) {
- t = Math.abs(Math.floor(t));
- const e = this.getPluralRulesForLocale();
- return e ? I18n.pluralRules[e](t) : "other"
+ selectPluralFormUsingFallbackRules(e) {
+ e = Math.abs(Math.floor(e));
+ const t = this.getPluralRulesForLocale();
+ return t ? I18n.pluralRules[t](e) : "other"
}
getPluralRulesForLocale() {
- const t = this.locale.split("-")[0];
- for (const e in I18n.pluralRulesMap) {
- const n = I18n.pluralRulesMap[e];
- if (n.includes(this.locale) || n.includes(t)) return e
+ const e = this.locale.split("-")[0];
+ for (const t in I18n.pluralRulesMap) {
+ const n = I18n.pluralRulesMap[t];
+ if (n.includes(this.locale) || n.includes(e)) return t
}
}
}
@@ -175,56 +192,56 @@ I18n.pluralRulesMap = {
spanish: ["pt-PT", "it", "es"],
welsh: ["cy"]
}, I18n.pluralRules = {
- arabic: t => 0 === t ? "zero" : 1 === t ? "one" : 2 === t ? "two" : t % 100 >= 3 && t % 100 <= 10 ? "few" : t % 100 >= 11 && t % 100 <= 99 ? "many" : "other",
+ arabic: e => 0 === e ? "zero" : 1 === e ? "one" : 2 === e ? "two" : e % 100 >= 3 && e % 100 <= 10 ? "few" : e % 100 >= 11 && e % 100 <= 99 ? "many" : "other",
chinese: () => "other",
- french: t => 0 === t || 1 === t ? "one" : "other",
- german: t => 1 === t ? "one" : "other",
- irish: t => 1 === t ? "one" : 2 === t ? "two" : t >= 3 && t <= 6 ? "few" : t >= 7 && t <= 10 ? "many" : "other",
- russian(t) {
- const e = t % 100,
- n = e % 10;
- return 1 === n && 11 !== e ? "one" : n >= 2 && n <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || e >= 11 && e <= 14 ? "many" : "other"
+ french: e => 0 === e || 1 === e ? "one" : "other",
+ german: e => 1 === e ? "one" : "other",
+ irish: e => 1 === e ? "one" : 2 === e ? "two" : e >= 3 && e <= 6 ? "few" : e >= 7 && e <= 10 ? "many" : "other",
+ russian(e) {
+ const t = e % 100,
+ n = t % 10;
+ return 1 === n && 11 !== t ? "one" : n >= 2 && n <= 4 && !(t >= 12 && t <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || t >= 11 && t <= 14 ? "many" : "other"
},
- scottish: t => 1 === t || 11 === t ? "one" : 2 === t || 12 === t ? "two" : t >= 3 && t <= 10 || t >= 13 && t <= 19 ? "few" : "other",
- spanish: t => 1 === t ? "one" : t % 1e6 == 0 && 0 !== t ? "many" : "other",
- welsh: t => 0 === t ? "zero" : 1 === t ? "one" : 2 === t ? "two" : 3 === t ? "few" : 6 === t ? "many" : "other"
+ scottish: e => 1 === e || 11 === e ? "one" : 2 === e || 12 === e ? "two" : e >= 3 && e <= 10 || e >= 13 && e <= 19 ? "few" : "other",
+ spanish: e => 1 === e ? "one" : e % 1e6 == 0 && 0 !== e ? "many" : "other",
+ welsh: e => 0 === e ? "zero" : 1 === e ? "one" : 2 === e ? "two" : 3 === e ? "few" : 6 === e ? "many" : "other"
};
class Accordion extends GOVUKFrontendComponent {
- constructor(e, n = {}) {
- if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.browserSupportsSessionStorage = !1, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(e instanceof HTMLElement)) throw new ElementError({
+ constructor(t, n = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.browserSupportsSessionStorage = !1, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(t instanceof HTMLElement)) throw new ElementError({
componentName: "Accordion",
- element: e,
+ element: t,
identifier: "Root element (`$module`)"
});
- this.$module = e, this.config = mergeConfigs(Accordion.defaults, n, normaliseDataset(e.dataset)), this.i18n = new I18n(extractConfigByNamespace(this.config, "i18n"));
+ this.$module = t, this.config = mergeConfigs(Accordion.defaults, n, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
const i = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!i.length) throw new ElementError({
componentName: "Accordion",
identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
});
- this.$sections = i, this.browserSupportsSessionStorage = t.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
+ this.$sections = i, this.browserSupportsSessionStorage = e.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
const s = this.checkIfAllSectionsOpen();
this.updateShowAllButton(s)
}
initControls() {
this.$showAllButton = document.createElement("button"), this.$showAllButton.setAttribute("type", "button"), this.$showAllButton.setAttribute("class", this.showAllClass), this.$showAllButton.setAttribute("aria-expanded", "false"), this.$showAllIcon = document.createElement("span"), this.$showAllIcon.classList.add(this.upChevronIconClass), this.$showAllButton.appendChild(this.$showAllIcon);
- const t = document.createElement("div");
- t.setAttribute("class", this.controlsClass), t.appendChild(this.$showAllButton), this.$module.insertBefore(t, this.$module.firstChild), this.$showAllText = document.createElement("span"), this.$showAllText.classList.add(this.showAllTextClass), this.$showAllButton.appendChild(this.$showAllText), this.$showAllButton.addEventListener("click", (() => this.onShowOrHideAllToggle())), "onbeforematch" in document && document.addEventListener("beforematch", (t => this.onBeforeMatch(t)))
+ const e = document.createElement("div");
+ e.setAttribute("class", this.controlsClass), e.appendChild(this.$showAllButton), this.$module.insertBefore(e, this.$module.firstChild), this.$showAllText = document.createElement("span"), this.$showAllText.classList.add(this.showAllTextClass), this.$showAllButton.appendChild(this.$showAllText), this.$showAllButton.addEventListener("click", (() => this.onShowOrHideAllToggle())), "onbeforematch" in document && document.addEventListener("beforematch", (e => this.onBeforeMatch(e)))
}
initSectionHeaders() {
- this.$sections.forEach(((t, e) => {
- const n = t.querySelector(`.${this.sectionHeaderClass}`);
+ this.$sections.forEach(((e, t) => {
+ const n = e.querySelector(`.${this.sectionHeaderClass}`);
if (!n) throw new ElementError({
componentName: "Accordion",
identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
});
- this.constructHeaderMarkup(n, e), this.setExpanded(this.isExpanded(t), t), n.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
+ this.constructHeaderMarkup(n, t), this.setExpanded(this.isExpanded(e), e), n.addEventListener("click", (() => this.onSectionToggle(e))), this.setInitialState(e)
}))
}
- constructHeaderMarkup(t, e) {
- const n = t.querySelector(`.${this.sectionButtonClass}`),
- i = t.querySelector(`.${this.sectionHeadingClass}`),
- s = t.querySelector(`.${this.sectionSummaryClass}`);
+ constructHeaderMarkup(e, t) {
+ const n = e.querySelector(`.${this.sectionButtonClass}`),
+ i = e.querySelector(`.${this.sectionHeadingClass}`),
+ s = e.querySelector(`.${this.sectionSummaryClass}`);
if (!i) throw new ElementError({
componentName: "Accordion",
identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
@@ -234,7 +251,7 @@ class Accordion extends GOVUKFrontendComponent {
identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
});
const o = document.createElement("button");
- o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$module.id}-content-${e+1}`);
+ o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$module.id}-content-${t+1}`);
for (const d of Array.from(n.attributes)) "id" !== d.nodeName && o.setAttribute(d.nodeName, `${d.nodeValue}`);
const r = document.createElement("span");
r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
@@ -247,84 +264,84 @@ class Accordion extends GOVUKFrontendComponent {
const h = document.createElement("span"),
u = document.createElement("span");
if (u.classList.add(this.upChevronIconClass), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), null != s && s.parentNode) {
- const t = document.createElement("span"),
- e = document.createElement("span");
- e.classList.add(this.sectionSummaryFocusClass), t.appendChild(e);
- for (const n of Array.from(s.attributes)) t.setAttribute(n.nodeName, `${n.nodeValue}`);
- e.innerHTML = s.innerHTML, s.parentNode.replaceChild(t, s), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
+ const e = document.createElement("span"),
+ t = document.createElement("span");
+ t.classList.add(this.sectionSummaryFocusClass), e.appendChild(t);
+ for (const n of Array.from(s.attributes)) e.setAttribute(n.nodeName, `${n.nodeValue}`);
+ t.innerHTML = s.innerHTML, s.parentNode.replaceChild(e, s), o.appendChild(e), o.appendChild(this.getButtonPunctuationEl())
}
o.appendChild(l), i.removeChild(n), i.appendChild(o)
}
- onBeforeMatch(t) {
- const e = t.target;
- if (!(e instanceof Element)) return;
- const n = e.closest(`.${this.sectionClass}`);
+ onBeforeMatch(e) {
+ const t = e.target;
+ if (!(t instanceof Element)) return;
+ const n = t.closest(`.${this.sectionClass}`);
n && this.setExpanded(!0, n)
}
- onSectionToggle(t) {
- const e = this.isExpanded(t);
- this.setExpanded(!e, t), this.storeState(t)
+ onSectionToggle(e) {
+ const t = this.isExpanded(e);
+ this.setExpanded(!t, e), this.storeState(e)
}
onShowOrHideAllToggle() {
- const t = !this.checkIfAllSectionsOpen();
- this.$sections.forEach((e => {
- this.setExpanded(t, e), this.storeState(e)
- })), this.updateShowAllButton(t)
- }
- setExpanded(t, e) {
- const n = e.querySelector(`.${this.upChevronIconClass}`),
- i = e.querySelector(`.${this.sectionShowHideTextClass}`),
- s = e.querySelector(`.${this.sectionButtonClass}`),
- o = e.querySelector(`.${this.sectionContentClass}`);
+ const e = !this.checkIfAllSectionsOpen();
+ this.$sections.forEach((t => {
+ this.setExpanded(e, t), this.storeState(t)
+ })), this.updateShowAllButton(e)
+ }
+ setExpanded(e, t) {
+ const n = t.querySelector(`.${this.upChevronIconClass}`),
+ i = t.querySelector(`.${this.sectionShowHideTextClass}`),
+ s = t.querySelector(`.${this.sectionButtonClass}`),
+ o = t.querySelector(`.${this.sectionContentClass}`);
if (!o) throw new ElementError({
componentName: "Accordion",
identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
});
if (!n || !i || !s) return;
- const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
- i.textContent = r, s.setAttribute("aria-expanded", `${t}`);
+ const r = e ? this.i18n.t("hideSection") : this.i18n.t("showSection");
+ i.textContent = r, s.setAttribute("aria-expanded", `${e}`);
const a = [],
- l = e.querySelector(`.${this.sectionHeadingTextClass}`);
+ l = t.querySelector(`.${this.sectionHeadingTextClass}`);
l && a.push(`${l.textContent}`.trim());
- const c = e.querySelector(`.${this.sectionSummaryClass}`);
+ const c = t.querySelector(`.${this.sectionSummaryClass}`);
c && a.push(`${c.textContent}`.trim());
- const h = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
- a.push(h), s.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass));
+ const h = e ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
+ a.push(h), s.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass));
const u = this.checkIfAllSectionsOpen();
this.updateShowAllButton(u)
}
- isExpanded(t) {
- return t.classList.contains(this.sectionExpandedClass)
+ isExpanded(e) {
+ return e.classList.contains(this.sectionExpandedClass)
}
checkIfAllSectionsOpen() {
return this.$sections.length === this.$module.querySelectorAll(`.${this.sectionExpandedClass}`).length
}
- updateShowAllButton(t) {
- this.$showAllButton && this.$showAllText && this.$showAllIcon && (this.$showAllButton.setAttribute("aria-expanded", t.toString()), this.$showAllText.textContent = t ? this.i18n.t("hideAllSections") : this.i18n.t("showAllSections"), this.$showAllIcon.classList.toggle(this.downChevronIconClass, !t))
+ updateShowAllButton(e) {
+ this.$showAllButton && this.$showAllText && this.$showAllIcon && (this.$showAllButton.setAttribute("aria-expanded", e.toString()), this.$showAllText.textContent = e ? this.i18n.t("hideAllSections") : this.i18n.t("showAllSections"), this.$showAllIcon.classList.toggle(this.downChevronIconClass, !e))
}
- storeState(t) {
+ storeState(e) {
if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
- const e = t.querySelector(`.${this.sectionButtonClass}`);
- if (e) {
- const t = e.getAttribute("aria-controls"),
- n = e.getAttribute("aria-expanded");
- t && n && window.sessionStorage.setItem(t, n)
+ const t = e.querySelector(`.${this.sectionButtonClass}`);
+ if (t) {
+ const e = t.getAttribute("aria-controls"),
+ n = t.getAttribute("aria-expanded");
+ e && n && window.sessionStorage.setItem(e, n)
}
}
}
- setInitialState(t) {
+ setInitialState(e) {
if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
- const e = t.querySelector(`.${this.sectionButtonClass}`);
- if (e) {
- const n = e.getAttribute("aria-controls"),
+ const t = e.querySelector(`.${this.sectionButtonClass}`);
+ if (t) {
+ const n = t.getAttribute("aria-controls"),
i = n ? window.sessionStorage.getItem(n) : null;
- null !== i && this.setExpanded("true" === i, t)
+ null !== i && this.setExpanded("true" === i, e)
}
}
}
getButtonPunctuationEl() {
- const t = document.createElement("span");
- return t.classList.add("govuk-visually-hidden", this.sectionHeadingDividerClass), t.innerHTML = ", ", t
+ const e = document.createElement("span");
+ return e.classList.add("govuk-visually-hidden", this.sectionHeadingDividerClass), e.innerHTML = ", ", e
}
}
Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
@@ -337,83 +354,100 @@ Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
showSectionAriaLabel: "Show this section"
},
rememberExpanded: !0
+}), Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: "object"
+ },
+ rememberExpanded: {
+ type: "boolean"
+ }
+ }
});
-const t = {
+const e = {
checkForSessionStorage: function() {
- const t = "this is the test string";
- let e;
+ const e = "this is the test string";
+ let t;
try {
- return window.sessionStorage.setItem(t, t), e = window.sessionStorage.getItem(t) === t.toString(), window.sessionStorage.removeItem(t), e
+ return window.sessionStorage.setItem(e, e), t = window.sessionStorage.getItem(e) === e.toString(), window.sessionStorage.removeItem(e), t
} catch (n) {
return !1
}
}
};
class Button extends GOVUKFrontendComponent {
- constructor(t, e = {}) {
- if (super(), this.$module = void 0, this.config = void 0, this.debounceFormSubmitTimer = null, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e, t = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, this.debounceFormSubmitTimer = null, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Button",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- this.$module = t, this.config = mergeConfigs(Button.defaults, e, normaliseDataset(t.dataset)), this.$module.addEventListener("keydown", (t => this.handleKeyDown(t))), this.$module.addEventListener("click", (t => this.debounce(t)))
+ this.$module = e, this.config = mergeConfigs(Button.defaults, t, normaliseDataset(Button, e.dataset)), this.$module.addEventListener("keydown", (e => this.handleKeyDown(e))), this.$module.addEventListener("click", (e => this.debounce(e)))
}
- handleKeyDown(t) {
- const e = t.target;
- " " === t.key && e instanceof HTMLElement && "button" === e.getAttribute("role") && (t.preventDefault(), e.click())
+ handleKeyDown(e) {
+ const t = e.target;
+ " " === e.key && t instanceof HTMLElement && "button" === t.getAttribute("role") && (e.preventDefault(), t.click())
}
- debounce(t) {
- if (this.config.preventDoubleClick) return this.debounceFormSubmitTimer ? (t.preventDefault(), !1) : void(this.debounceFormSubmitTimer = window.setTimeout((() => {
+ debounce(e) {
+ if (this.config.preventDoubleClick) return this.debounceFormSubmitTimer ? (e.preventDefault(), !1) : void(this.debounceFormSubmitTimer = window.setTimeout((() => {
this.debounceFormSubmitTimer = null
}), 1e3))
}
}
-function closestAttributeValue(t, e) {
- const n = t.closest(`[${e}]`);
- return n ? n.getAttribute(e) : null
+function closestAttributeValue(e, t) {
+ const n = e.closest(`[${t}]`);
+ return n ? n.getAttribute(t) : null
}
Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
preventDoubleClick: !1
+}), Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: "boolean"
+ }
+ }
});
class CharacterCount extends GOVUKFrontendComponent {
- constructor(t, e = {}) {
+ constructor(e, t = {}) {
var n, i;
- if (super(), this.$module = void 0, this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, this.i18n = void 0, this.maxLength = void 0, !(t instanceof HTMLElement)) throw new ElementError({
+ if (super(), this.$module = void 0, this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, this.i18n = void 0, this.maxLength = void 0, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Character count",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- const s = t.querySelector(".govuk-js-character-count");
+ const s = e.querySelector(".govuk-js-character-count");
if (!(s instanceof HTMLTextAreaElement || s instanceof HTMLInputElement)) throw new ElementError({
componentName: "Character count",
element: s,
expectedType: "HTMLTextareaElement or HTMLInputElement",
identifier: "Form field (`.govuk-js-character-count`)"
});
- const o = normaliseDataset(t.dataset);
+ const o = normaliseDataset(CharacterCount, e.dataset);
let r = {};
("maxwords" in o || "maxlength" in o) && (r = {
maxlength: void 0,
maxwords: void 0
- }), this.config = mergeConfigs(CharacterCount.defaults, e, r, o);
- const a = function(t, e) {
+ }), this.config = mergeConfigs(CharacterCount.defaults, t, r, o);
+ const a = function(e, t) {
const n = [];
- for (const [i, s] of Object.entries(t)) {
- const t = [];
- for (const {
- required: n,
- errorMessage: i
- }
- of s) n.every((t => !!e[t])) || t.push(i);
- "anyOf" !== i || s.length - t.length >= 1 || n.push(...t)
+ for (const [i, s] of Object.entries(e)) {
+ const e = [];
+ if (Array.isArray(s)) {
+ for (const {
+ required: n,
+ errorMessage: i
+ }
+ of s) n.every((e => !!t[e])) || e.push(i);
+ "anyOf" !== i || s.length - e.length >= 1 || n.push(...e)
+ }
}
return n
}(CharacterCount.schema, this.config);
if (a[0]) throw new ConfigError(`Character count: ${a[0]}`);
- this.i18n = new I18n(extractConfigByNamespace(this.config, "i18n"), {
- locale: closestAttributeValue(t, "lang")
- }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$module = t, this.$textarea = s;
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(e, "lang")
+ }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$module = e, this.$textarea = s;
const l = `${this.$textarea.id}-info`,
c = document.getElementById(l);
if (!c) throw new ElementError({
@@ -450,35 +484,35 @@ class CharacterCount extends GOVUKFrontendComponent {
this.updateVisibleCountMessage(), this.updateScreenReaderCountMessage()
}
updateVisibleCountMessage() {
- const t = this.maxLength - this.count(this.$textarea.value) < 0;
- this.$visibleCountMessage.classList.toggle("govuk-character-count__message--disabled", !this.isOverThreshold()), this.$textarea.classList.toggle("govuk-textarea--error", t), this.$visibleCountMessage.classList.toggle("govuk-error-message", t), this.$visibleCountMessage.classList.toggle("govuk-hint", !t), this.$visibleCountMessage.textContent = this.getCountMessage()
+ const e = this.maxLength - this.count(this.$textarea.value) < 0;
+ this.$visibleCountMessage.classList.toggle("govuk-character-count__message--disabled", !this.isOverThreshold()), this.$textarea.classList.toggle("govuk-textarea--error", e), this.$visibleCountMessage.classList.toggle("govuk-error-message", e), this.$visibleCountMessage.classList.toggle("govuk-hint", !e), this.$visibleCountMessage.textContent = this.getCountMessage()
}
updateScreenReaderCountMessage() {
this.isOverThreshold() ? this.$screenReaderCountMessage.removeAttribute("aria-hidden") : this.$screenReaderCountMessage.setAttribute("aria-hidden", "true"), this.$screenReaderCountMessage.textContent = this.getCountMessage()
}
- count(t) {
+ count(e) {
if (this.config.maxwords) {
- var e;
- return (null != (e = t.match(/\S+/g)) ? e : []).length
+ var t;
+ return (null != (t = e.match(/\S+/g)) ? t : []).length
}
- return t.length
+ return e.length
}
getCountMessage() {
- const t = this.maxLength - this.count(this.$textarea.value),
- e = this.config.maxwords ? "words" : "characters";
- return this.formatCountMessage(t, e)
- }
- formatCountMessage(t, e) {
- if (0 === t) return this.i18n.t(`${e}AtLimit`);
- const n = t < 0 ? "OverLimit" : "UnderLimit";
- return this.i18n.t(`${e}${n}`, {
- count: Math.abs(t)
+ const e = this.maxLength - this.count(this.$textarea.value),
+ t = this.config.maxwords ? "words" : "characters";
+ return this.formatCountMessage(e, t)
+ }
+ formatCountMessage(e, t) {
+ if (0 === e) return this.i18n.t(`${t}AtLimit`);
+ const n = e < 0 ? "OverLimit" : "UnderLimit";
+ return this.i18n.t(`${t}${n}`, {
+ count: Math.abs(e)
})
}
isOverThreshold() {
if (!this.config.threshold) return !0;
- const t = this.count(this.$textarea.value);
- return this.maxLength * this.config.threshold / 100 <= t
+ const e = this.count(this.$textarea.value);
+ return this.maxLength * this.config.threshold / 100 <= e
}
}
CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = Object.freeze({
@@ -507,6 +541,20 @@ CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = O
}
}
}), CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: "object"
+ },
+ maxwords: {
+ type: "number"
+ },
+ maxlength: {
+ type: "number"
+ },
+ threshold: {
+ type: "number"
+ }
+ },
anyOf: [{
required: ["maxwords"],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
@@ -516,118 +564,124 @@ CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = O
}]
});
class Checkboxes extends GOVUKFrontendComponent {
- constructor(t) {
- if (super(), this.$module = void 0, this.$inputs = void 0, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e) {
+ if (super(), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Checkboxes",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- const e = t.querySelectorAll('input[type="checkbox"]');
- if (!e.length) throw new ElementError({
+ const t = e.querySelectorAll('input[type="checkbox"]');
+ if (!t.length) throw new ElementError({
componentName: "Checkboxes",
identifier: 'Form inputs (`<input type="checkbox">`)'
});
- this.$module = t, this.$inputs = e, this.$inputs.forEach((t => {
- const e = t.getAttribute("data-aria-controls");
- if (e) {
- if (!document.getElementById(e)) throw new ElementError({
+ this.$module = e, this.$inputs = t, this.$inputs.forEach((e => {
+ const t = e.getAttribute("data-aria-controls");
+ if (t) {
+ if (!document.getElementById(t)) throw new ElementError({
componentName: "Checkboxes",
- identifier: `Conditional reveal (\`id="${e}"\`)`
+ identifier: `Conditional reveal (\`id="${t}"\`)`
});
- t.setAttribute("aria-controls", e), t.removeAttribute("data-aria-controls")
+ e.setAttribute("aria-controls", t), e.removeAttribute("data-aria-controls")
}
- })), window.addEventListener("pageshow", (() => this.syncAllConditionalReveals())), this.syncAllConditionalReveals(), this.$module.addEventListener("click", (t => this.handleClick(t)))
+ })), window.addEventListener("pageshow", (() => this.syncAllConditionalReveals())), this.syncAllConditionalReveals(), this.$module.addEventListener("click", (e => this.handleClick(e)))
}
syncAllConditionalReveals() {
- this.$inputs.forEach((t => this.syncConditionalRevealWithInputState(t)))
+ this.$inputs.forEach((e => this.syncConditionalRevealWithInputState(e)))
}
- syncConditionalRevealWithInputState(t) {
- const e = t.getAttribute("aria-controls");
- if (!e) return;
- const n = document.getElementById(e);
+ syncConditionalRevealWithInputState(e) {
+ const t = e.getAttribute("aria-controls");
+ if (!t) return;
+ const n = document.getElementById(t);
if (n && n.classList.contains("govuk-checkboxes__conditional")) {
- const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
+ const t = e.checked;
+ e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !t)
}
}
- unCheckAllInputsExcept(t) {
- document.querySelectorAll(`input[type="checkbox"][name="${t.name}"]`).forEach((e => {
- t.form === e.form && e !== t && (e.checked = !1, this.syncConditionalRevealWithInputState(e))
+ unCheckAllInputsExcept(e) {
+ document.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`).forEach((t => {
+ e.form === t.form && t !== e && (t.checked = !1, this.syncConditionalRevealWithInputState(t))
}))
}
- unCheckExclusiveInputs(t) {
- document.querySelectorAll(`input[data-behaviour="exclusive"][type="checkbox"][name="${t.name}"]`).forEach((e => {
- t.form === e.form && (e.checked = !1, this.syncConditionalRevealWithInputState(e))
+ unCheckExclusiveInputs(e) {
+ document.querySelectorAll(`input[data-behaviour="exclusive"][type="checkbox"][name="${e.name}"]`).forEach((t => {
+ e.form === t.form && (t.checked = !1, this.syncConditionalRevealWithInputState(t))
}))
}
- handleClick(t) {
- const e = t.target;
- if (!(e instanceof HTMLInputElement) || "checkbox" !== e.type) return;
- if (e.getAttribute("aria-controls") && this.syncConditionalRevealWithInputState(e), !e.checked) return;
- "exclusive" === e.getAttribute("data-behaviour") ? this.unCheckAllInputsExcept(e) : this.unCheckExclusiveInputs(e)
+ handleClick(e) {
+ const t = e.target;
+ if (!(t instanceof HTMLInputElement) || "checkbox" !== t.type) return;
+ if (t.getAttribute("aria-controls") && this.syncConditionalRevealWithInputState(t), !t.checked) return;
+ "exclusive" === t.getAttribute("data-behaviour") ? this.unCheckAllInputsExcept(t) : this.unCheckExclusiveInputs(t)
}
}
Checkboxes.moduleName = "govuk-checkboxes";
class ErrorSummary extends GOVUKFrontendComponent {
- constructor(t, e = {}) {
- if (super(), this.$module = void 0, this.config = void 0, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e, t = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Error summary",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- this.$module = t, this.config = mergeConfigs(ErrorSummary.defaults, e, normaliseDataset(t.dataset)), this.config.disableAutoFocus || setFocus(this.$module), this.$module.addEventListener("click", (t => this.handleClick(t)))
+ this.$module = e, this.config = mergeConfigs(ErrorSummary.defaults, t, normaliseDataset(ErrorSummary, e.dataset)), this.config.disableAutoFocus || setFocus(this.$module), this.$module.addEventListener("click", (e => this.handleClick(e)))
}
- handleClick(t) {
- const e = t.target;
- e && this.focusTarget(e) && t.preventDefault()
+ handleClick(e) {
+ const t = e.target;
+ t && this.focusTarget(t) && e.preventDefault()
}
- focusTarget(t) {
- if (!(t instanceof HTMLAnchorElement)) return !1;
- const e = getFragmentFromUrl(t.href);
- if (!e) return !1;
- const n = document.getElementById(e);
+ focusTarget(e) {
+ if (!(e instanceof HTMLAnchorElement)) return !1;
+ const t = getFragmentFromUrl(e.href);
+ if (!t) return !1;
+ const n = document.getElementById(t);
if (!n) return !1;
const i = this.getAssociatedLegendOrLabel(n);
return !!i && (i.scrollIntoView(), n.focus({
preventScroll: !0
}), !0)
}
- getAssociatedLegendOrLabel(t) {
- var e;
- const n = t.closest("fieldset");
+ getAssociatedLegendOrLabel(e) {
+ var t;
+ const n = e.closest("fieldset");
if (n) {
- const e = n.getElementsByTagName("legend");
- if (e.length) {
- const n = e[0];
- if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
+ const t = n.getElementsByTagName("legend");
+ if (t.length) {
+ const n = t[0];
+ if (e instanceof HTMLInputElement && ("checkbox" === e.type || "radio" === e.type)) return n;
const i = n.getBoundingClientRect().top,
- s = t.getBoundingClientRect();
+ s = e.getBoundingClientRect();
if (s.height && window.innerHeight) {
if (s.top + s.height - i < window.innerHeight / 2) return n
}
}
}
- return null != (e = document.querySelector(`label[for='${t.getAttribute("id")}']`)) ? e : t.closest("label")
+ return null != (t = document.querySelector(`label[for='${e.getAttribute("id")}']`)) ? t : e.closest("label")
}
}
ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.freeze({
disableAutoFocus: !1
+}), ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: "boolean"
+ }
+ }
});
class ExitThisPage extends GOVUKFrontendComponent {
- constructor(t, e = {}) {
- if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e, t = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Exit this page",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- const n = t.querySelector(".govuk-exit-this-page__button");
+ const n = e.querySelector(".govuk-exit-this-page__button");
if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
componentName: "Exit this page",
element: n,
expectedType: "HTMLAnchorElement",
identifier: "Button (`.govuk-exit-this-page__button`)"
});
- this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(t.dataset)), this.i18n = new I18n(extractConfigByNamespace(this.config, "i18n")), this.$module = t, this.$button = n;
+ this.config = mergeConfigs(ExitThisPage.defaults, t, normaliseDataset(ExitThisPage, e.dataset)), this.i18n = new I18n(this.config.i18n), this.$module = e, this.$button = n;
const i = document.querySelector(".govuk-js-exit-this-page-skiplink");
i instanceof HTMLAnchorElement && (this.$skiplinkButton = i), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
}
@@ -639,27 +693,27 @@ class ExitThisPage extends GOVUKFrontendComponent {
}
buildIndicator() {
this.$indicatorContainer = document.createElement("div"), this.$indicatorContainer.className = "govuk-exit-this-page__indicator", this.$indicatorContainer.setAttribute("aria-hidden", "true");
- for (let t = 0; t < 3; t++) {
- const t = document.createElement("div");
- t.className = "govuk-exit-this-page__indicator-light", this.$indicatorContainer.appendChild(t)
+ for (let e = 0; e < 3; e++) {
+ const e = document.createElement("div");
+ e.className = "govuk-exit-this-page__indicator-light", this.$indicatorContainer.appendChild(e)
}
this.$button.appendChild(this.$indicatorContainer)
}
updateIndicator() {
if (!this.$indicatorContainer) return;
this.$indicatorContainer.classList.toggle("govuk-exit-this-page__indicator--visible", this.keypressCounter > 0);
- this.$indicatorContainer.querySelectorAll(".govuk-exit-this-page__indicator-light").forEach(((t, e) => {
- t.classList.toggle("govuk-exit-this-page__indicator-light--on", e < this.keypressCounter)
+ this.$indicatorContainer.querySelectorAll(".govuk-exit-this-page__indicator-light").forEach(((e, t) => {
+ e.classList.toggle("govuk-exit-this-page__indicator-light--on", t < this.keypressCounter)
}))
}
exitPage() {
this.$updateSpan && (this.$updateSpan.textContent = "", document.body.classList.add("govuk-exit-this-page-hide-content"), this.$overlay = document.createElement("div"), this.$overlay.className = "govuk-exit-this-page-overlay", this.$overlay.setAttribute("role", "alert"), document.body.appendChild(this.$overlay), this.$overlay.textContent = this.i18n.t("activated"), window.location.href = this.$button.href)
}
- handleClick(t) {
- t.preventDefault(), this.exitPage()
+ handleClick(e) {
+ e.preventDefault(), this.exitPage()
}
- handleKeypress(t) {
- this.$updateSpan && ("Shift" !== t.key || this.lastKeyWasModified ? this.keypressTimeoutId && this.resetKeypressTimer() : (this.keypressCounter += 1, this.updateIndicator(), this.timeoutMessageId && (window.clearTimeout(this.timeoutMessageId), this.timeoutMessageId = null), this.keypressCounter >= 3 ? (this.keypressCounter = 0, this.keypressTimeoutId && (window.clearTimeout(this.keypressTimeoutId), this.keypressTimeoutId = null), this.exitPage()) : 1 === this.keypressCounter ? this.$updateSpan.textContent = this.i18n.t("pressTwoMoreTimes") : this.$updateSpan.textContent = this.i18n.t("pressOneMoreTime"), this.setKeypressTimer()), this.lastKeyWasModified = t.shiftKey)
+ handleKeypress(e) {
+ this.$updateSpan && ("Shift" !== e.key || this.lastKeyWasModified ? this.keypressTimeoutId && this.resetKeypressTimer() : (this.keypressCounter += 1, this.updateIndicator(), this.timeoutMessageId && (window.clearTimeout(this.timeoutMessageId), this.timeoutMessageId = null), this.keypressCounter >= 3 ? (this.keypressCounter = 0, this.keypressTimeoutId && (window.clearTimeout(this.keypressTimeoutId), this.keypressTimeoutId = null), this.exitPage()) : 1 === this.keypressCounter ? this.$updateSpan.textContent = this.i18n.t("pressTwoMoreTimes") : this.$updateSpan.textContent = this.i18n.t("pressOneMoreTime"), this.setKeypressTimer()), this.lastKeyWasModified = e.shiftKey)
}
setKeypressTimer() {
this.keypressTimeoutId && window.clearTimeout(this.keypressTimeoutId), this.keypressTimeoutId = window.setTimeout(this.resetKeypressTimer.bind(this), this.timeoutTime)
@@ -667,9 +721,9 @@ class ExitThisPage extends GOVUKFrontendComponent {
resetKeypressTimer() {
if (!this.$updateSpan) return;
this.keypressTimeoutId && (window.clearTimeout(this.keypressTimeoutId), this.keypressTimeoutId = null);
- const t = this.$updateSpan;
- this.keypressCounter = 0, t.textContent = this.i18n.t("timedOut"), this.timeoutMessageId = window.setTimeout((() => {
- t.textContent = ""
+ const e = this.$updateSpan;
+ this.keypressCounter = 0, e.textContent = this.i18n.t("timedOut"), this.timeoutMessageId = window.setTimeout((() => {
+ e.textContent = ""
}), this.timeoutTime), this.updateIndicator()
}
resetPage() {
@@ -683,18 +737,24 @@ ExitThisPage.moduleName = "govuk-exit-this-page", ExitThisPage.defaults = Object
pressTwoMoreTimes: "Shift, press 2 more times to exit.",
pressOneMoreTime: "Shift, press 1 more time to exit."
}
+}), ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: "object"
+ }
+ }
});
class Header extends GOVUKFrontendComponent {
- constructor(t) {
- if (super(), this.$module = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !t) throw new ElementError({
+ constructor(e) {
+ if (super(), this.$module = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !e) throw new ElementError({
componentName: "Header",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- this.$module = t;
- const e = t.querySelector(".govuk-js-header-toggle");
- if (!e) return this;
- const n = e.getAttribute("aria-controls");
+ this.$module = e;
+ const t = e.querySelector(".govuk-js-header-toggle");
+ if (!t) return this;
+ const n = t.getAttribute("aria-controls");
if (!n) throw new ElementError({
componentName: "Header",
identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
@@ -705,15 +765,15 @@ class Header extends GOVUKFrontendComponent {
element: i,
identifier: `Navigation (\`<ul id="${n}">\`)`
});
- this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = i, this.$menuButton = t, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
- const t = getBreakpoint("desktop");
- if (!t.value) throw new ElementError({
+ const e = getBreakpoint("desktop");
+ if (!e.value) throw new ElementError({
componentName: "Header",
- identifier: `CSS custom property (\`${t.property}\`) on pseudo-class \`:root\``
+ identifier: `CSS custom property (\`${e.property}\`) on pseudo-class \`:root\``
});
- this.mql = window.matchMedia(`(min-width: ${t.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
+ this.mql = window.matchMedia(`(min-width: ${e.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
}
checkMode() {
this.mql && this.$menu && this.$menuButton && (this.mql.matches ? (this.$menu.removeAttribute("hidden"), this.$menuButton.setAttribute("hidden", "")) : (this.$menuButton.removeAttribute("hidden"), this.$menuButton.setAttribute("aria-expanded", this.menuIsOpen.toString()), this.menuIsOpen ? this.$menu.removeAttribute("hidden") : this.$menu.setAttribute("hidden", "")))
@@ -724,78 +784,84 @@ class Header extends GOVUKFrontendComponent {
}
Header.moduleName = "govuk-header";
class NotificationBanner extends GOVUKFrontendComponent {
- constructor(t, e = {}) {
- if (super(), this.$module = void 0, this.config = void 0, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e, t = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Notification banner",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- this.$module = t, this.config = mergeConfigs(NotificationBanner.defaults, e, normaliseDataset(t.dataset)), "alert" !== this.$module.getAttribute("role") || this.config.disableAutoFocus || setFocus(this.$module)
+ this.$module = e, this.config = mergeConfigs(NotificationBanner.defaults, t, normaliseDataset(NotificationBanner, e.dataset)), "alert" !== this.$module.getAttribute("role") || this.config.disableAutoFocus || setFocus(this.$module)
}
}
NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.defaults = Object.freeze({
disableAutoFocus: !1
+}), NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: "boolean"
+ }
+ }
});
class Radios extends GOVUKFrontendComponent {
- constructor(t) {
- if (super(), this.$module = void 0, this.$inputs = void 0, !(t instanceof HTMLElement)) throw new ElementError({
+ constructor(e) {
+ if (super(), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Radios",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- const e = t.querySelectorAll('input[type="radio"]');
- if (!e.length) throw new ElementError({
+ const t = e.querySelectorAll('input[type="radio"]');
+ if (!t.length) throw new ElementError({
componentName: "Radios",
identifier: 'Form inputs (`<input type="radio">`)'
});
- this.$module = t, this.$inputs = e, this.$inputs.forEach((t => {
- const e = t.getAttribute("data-aria-controls");
- if (e) {
- if (!document.getElementById(e)) throw new ElementError({
+ this.$module = e, this.$inputs = t, this.$inputs.forEach((e => {
+ const t = e.getAttribute("data-aria-controls");
+ if (t) {
+ if (!document.getElementById(t)) throw new ElementError({
componentName: "Radios",
- identifier: `Conditional reveal (\`id="${e}"\`)`
+ identifier: `Conditional reveal (\`id="${t}"\`)`
});
- t.setAttribute("aria-controls", e), t.removeAttribute("data-aria-controls")
+ e.setAttribute("aria-controls", t), e.removeAttribute("data-aria-controls")
}
- })), window.addEventListener("pageshow", (() => this.syncAllConditionalReveals())), this.syncAllConditionalReveals(), this.$module.addEventListener("click", (t => this.handleClick(t)))
+ })), window.addEventListener("pageshow", (() => this.syncAllConditionalReveals())), this.syncAllConditionalReveals(), this.$module.addEventListener("click", (e => this.handleClick(e)))
}
syncAllConditionalReveals() {
- this.$inputs.forEach((t => this.syncConditionalRevealWithInputState(t)))
+ this.$inputs.forEach((e => this.syncConditionalRevealWithInputState(e)))
}
- syncConditionalRevealWithInputState(t) {
- const e = t.getAttribute("aria-controls");
- if (!e) return;
- const n = document.getElementById(e);
+ syncConditionalRevealWithInputState(e) {
+ const t = e.getAttribute("aria-controls");
+ if (!t) return;
+ const n = document.getElementById(t);
if (null != n && n.classList.contains("govuk-radios__conditional")) {
- const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !e)
+ const t = e.checked;
+ e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !t)
}
}
- handleClick(t) {
- const e = t.target;
- if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
+ handleClick(e) {
+ const t = e.target;
+ if (!(t instanceof HTMLInputElement) || "radio" !== t.type) return;
const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
- i = e.form,
- s = e.name;
- n.forEach((t => {
- const e = t.form === i;
- t.name === s && e && this.syncConditionalRevealWithInputState(t)
+ i = t.form,
+ s = t.name;
+ n.forEach((e => {
+ const t = e.form === i;
+ e.name === s && t && this.syncConditionalRevealWithInputState(e)
}))
}
}
Radios.moduleName = "govuk-radios";
class SkipLink extends GOVUKFrontendComponent {
- constructor(t) {
- var e;
- if (super(), this.$module = void 0, !(t instanceof HTMLAnchorElement)) throw new ElementError({
+ constructor(e) {
+ var t;
+ if (super(), this.$module = void 0, !(e instanceof HTMLAnchorElement)) throw new ElementError({
componentName: "Skip link",
- element: t,
+ element: e,
expectedType: "HTMLAnchorElement",
identifier: "Root element (`$module`)"
});
- this.$module = t;
+ this.$module = e;
const n = this.$module.hash,
- i = null != (e = this.$module.getAttribute("href")) ? e : "";
+ i = null != (t = this.$module.getAttribute("href")) ? t : "";
let s;
try {
s = new window.URL(this.$module.href)
@@ -823,18 +889,18 @@ class SkipLink extends GOVUKFrontendComponent {
}
SkipLink.moduleName = "govuk-skip-link";
class Tabs extends GOVUKFrontendComponent {
- constructor(t) {
- if (super(), this.$module = void 0, this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null, !t) throw new ElementError({
+ constructor(e) {
+ if (super(), this.$module = void 0, this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null, !e) throw new ElementError({
componentName: "Tabs",
- element: t,
+ element: e,
identifier: "Root element (`$module`)"
});
- const e = t.querySelectorAll("a.govuk-tabs__tab");
- if (!e.length) throw new ElementError({
+ const t = e.querySelectorAll("a.govuk-tabs__tab");
+ if (!t.length) throw new ElementError({
componentName: "Tabs",
identifier: 'Links (`<a class="govuk-tabs__tab">`)'
});
- this.$module = t, this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
+ this.$module = e, this.$tabs = t, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
const n = this.$module.querySelector(".govuk-tabs__list"),
i = this.$module.querySelectorAll("li.govuk-tabs__list-item");
if (!n) throw new ElementError({
@@ -848,149 +914,149 @@ class Tabs extends GOVUKFrontendComponent {
this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
}
setupResponsiveChecks() {
- const t = getBreakpoint("tablet");
- if (!t.value) throw new ElementError({
+ const e = getBreakpoint("tablet");
+ if (!e.value) throw new ElementError({
componentName: "Tabs",
- identifier: `CSS custom property (\`${t.property}\`) on pseudo-class \`:root\``
+ identifier: `CSS custom property (\`${e.property}\`) on pseudo-class \`:root\``
});
- this.mql = window.matchMedia(`(min-width: ${t.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
+ this.mql = window.matchMedia(`(min-width: ${e.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
}
checkMode() {
- var t;
- null != (t = this.mql) && t.matches ? this.setup() : this.teardown()
+ var e;
+ null != (e = this.mql) && e.matches ? this.setup() : this.teardown()
}
setup() {
- var t;
- this.$tabList.setAttribute("role", "tablist"), this.$tabListItems.forEach((t => {
- t.setAttribute("role", "presentation")
- })), this.$tabs.forEach((t => {
- this.setAttributes(t), t.addEventListener("click", this.boundTabClick, !0), t.addEventListener("keydown", this.boundTabKeydown, !0), this.hideTab(t)
+ var e;
+ this.$tabList.setAttribute("role", "tablist"), this.$tabListItems.forEach((e => {
+ e.setAttribute("role", "presentation")
+ })), this.$tabs.forEach((e => {
+ this.setAttributes(e), e.addEventListener("click", this.boundTabClick, !0), e.addEventListener("keydown", this.boundTabKeydown, !0), this.hideTab(e)
}));
- const e = null != (t = this.getTab(window.location.hash)) ? t : this.$tabs[0];
- this.showTab(e), window.addEventListener("hashchange", this.boundOnHashChange, !0)
+ const t = null != (e = this.getTab(window.location.hash)) ? e : this.$tabs[0];
+ this.showTab(t), window.addEventListener("hashchange", this.boundOnHashChange, !0)
}
teardown() {
- this.$tabList.removeAttribute("role"), this.$tabListItems.forEach((t => {
- t.removeAttribute("role")
- })), this.$tabs.forEach((t => {
- t.removeEventListener("click", this.boundTabClick, !0), t.removeEventListener("keydown", this.boundTabKeydown, !0), this.unsetAttributes(t)
+ this.$tabList.removeAttribute("role"), this.$tabListItems.forEach((e => {
+ e.removeAttribute("role")
+ })), this.$tabs.forEach((e => {
+ e.removeEventListener("click", this.boundTabClick, !0), e.removeEventListener("keydown", this.boundTabKeydown, !0), this.unsetAttributes(e)
})), window.removeEventListener("hashchange", this.boundOnHashChange, !0)
}
onHashChange() {
- const t = window.location.hash,
- e = this.getTab(t);
- if (!e) return;
+ const e = window.location.hash,
+ t = this.getTab(e);
+ if (!t) return;
if (this.changingHash) return void(this.changingHash = !1);
const n = this.getCurrentTab();
- n && (this.hideTab(n), this.showTab(e), e.focus())
- }
- hideTab(t) {
- this.unhighlightTab(t), this.hidePanel(t)
- }
- showTab(t) {
- this.highlightTab(t), this.showPanel(t)
- }
- getTab(t) {
- return this.$module.querySelector(`a.govuk-tabs__tab[href="${t}"]`)
- }
- setAttributes(t) {
- const e = getFragmentFromUrl(t.href);
- if (!e) return;
- t.setAttribute("id", `tab_${e}`), t.setAttribute("role", "tab"), t.setAttribute("aria-controls", e), t.setAttribute("aria-selected", "false"), t.setAttribute("tabindex", "-1");
- const n = this.getPanel(t);
- n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", t.id), n.classList.add(this.jsHiddenClass))
- }
- unsetAttributes(t) {
- t.removeAttribute("id"), t.removeAttribute("role"), t.removeAttribute("aria-controls"), t.removeAttribute("aria-selected"), t.removeAttribute("tabindex");
- const e = this.getPanel(t);
- e && (e.removeAttribute("role"), e.removeAttribute("aria-labelledby"), e.classList.remove(this.jsHiddenClass))
- }
- onTabClick(t) {
- const e = this.getCurrentTab(),
- n = t.currentTarget;
- e && n instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(n), this.createHistoryEntry(n))
- }
- createHistoryEntry(t) {
- const e = this.getPanel(t);
- if (!e) return;
- const n = e.id;
- e.id = "", this.changingHash = !0, window.location.hash = n, e.id = n
- }
- onTabKeydown(t) {
- switch (t.key) {
+ n && (this.hideTab(n), this.showTab(t), t.focus())
+ }
+ hideTab(e) {
+ this.unhighlightTab(e), this.hidePanel(e)
+ }
+ showTab(e) {
+ this.highlightTab(e), this.showPanel(e)
+ }
+ getTab(e) {
+ return this.$module.querySelector(`a.govuk-tabs__tab[href="${e}"]`)
+ }
+ setAttributes(e) {
+ const t = getFragmentFromUrl(e.href);
+ if (!t) return;
+ e.setAttribute("id", `tab_${t}`), e.setAttribute("role", "tab"), e.setAttribute("aria-controls", t), e.setAttribute("aria-selected", "false"), e.setAttribute("tabindex", "-1");
+ const n = this.getPanel(e);
+ n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", e.id), n.classList.add(this.jsHiddenClass))
+ }
+ unsetAttributes(e) {
+ e.removeAttribute("id"), e.removeAttribute("role"), e.removeAttribute("aria-controls"), e.removeAttribute("aria-selected"), e.removeAttribute("tabindex");
+ const t = this.getPanel(e);
+ t && (t.removeAttribute("role"), t.removeAttribute("aria-labelledby"), t.classList.remove(this.jsHiddenClass))
+ }
+ onTabClick(e) {
+ const t = this.getCurrentTab(),
+ n = e.currentTarget;
+ t && n instanceof HTMLAnchorElement && (e.preventDefault(), this.hideTab(t), this.showTab(n), this.createHistoryEntry(n))
+ }
+ createHistoryEntry(e) {
+ const t = this.getPanel(e);
+ if (!t) return;
+ const n = t.id;
+ t.id = "", this.changingHash = !0, window.location.hash = n, t.id = n
+ }
+ onTabKeydown(e) {
+ switch (e.key) {
case "ArrowLeft":
case "ArrowUp":
case "Left":
case "Up":
- this.activatePreviousTab(), t.preventDefault();
+ this.activatePreviousTab(), e.preventDefault();
break;
case "ArrowRight":
case "ArrowDown":
case "Right":
case "Down":
- this.activateNextTab(), t.preventDefault()
+ this.activateNextTab(), e.preventDefault()
}
}
activateNextTab() {
- const t = this.getCurrentTab();
- if (null == t || !t.parentElement) return;
- const e = t.parentElement.nextElementSibling;
- if (!e) return;
- const n = e.querySelector("a.govuk-tabs__tab");
- n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+ const e = this.getCurrentTab();
+ if (null == e || !e.parentElement) return;
+ const t = e.parentElement.nextElementSibling;
+ if (!t) return;
+ const n = t.querySelector("a.govuk-tabs__tab");
+ n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
}
activatePreviousTab() {
- const t = this.getCurrentTab();
- if (null == t || !t.parentElement) return;
- const e = t.parentElement.previousElementSibling;
- if (!e) return;
- const n = e.querySelector("a.govuk-tabs__tab");
- n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+ const e = this.getCurrentTab();
+ if (null == e || !e.parentElement) return;
+ const t = e.parentElement.previousElementSibling;
+ if (!t) return;
+ const n = t.querySelector("a.govuk-tabs__tab");
+ n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
}
- getPanel(t) {
- const e = getFragmentFromUrl(t.href);
- return e ? this.$module.querySelector(`#${e}`) : null
+ getPanel(e) {
+ const t = getFragmentFromUrl(e.href);
+ return t ? this.$module.querySelector(`#${t}`) : null
}
- showPanel(t) {
- const e = this.getPanel(t);
- e && e.classList.remove(this.jsHiddenClass)
+ showPanel(e) {
+ const t = this.getPanel(e);
+ t && t.classList.remove(this.jsHiddenClass)
}
- hidePanel(t) {
- const e = this.getPanel(t);
- e && e.classList.add(this.jsHiddenClass)
+ hidePanel(e) {
+ const t = this.getPanel(e);
+ t && t.classList.add(this.jsHiddenClass)
}
- unhighlightTab(t) {
- t.parentElement && (t.setAttribute("aria-selected", "false"), t.parentElement.classList.remove("govuk-tabs__list-item--selected"), t.setAttribute("tabindex", "-1"))
+ unhighlightTab(e) {
+ e.parentElement && (e.setAttribute("aria-selected", "false"), e.parentElement.classList.remove("govuk-tabs__list-item--selected"), e.setAttribute("tabindex", "-1"))
}
- highlightTab(t) {
- t.parentElement && (t.setAttribute("aria-selected", "true"), t.parentElement.classList.add("govuk-tabs__list-item--selected"), t.setAttribute("tabindex", "0"))
+ highlightTab(e) {
+ e.parentElement && (e.setAttribute("aria-selected", "true"), e.parentElement.classList.add("govuk-tabs__list-item--selected"), e.setAttribute("tabindex", "0"))
}
getCurrentTab() {
return this.$module.querySelector(".govuk-tabs__list-item--selected a.govuk-tabs__tab")
}
}
-function initAll(t) {
- var e;
- if (t = void 0 !== t ? t : {}, !isSupported()) return void console.log(new SupportError);
+function initAll(e) {
+ var t;
+ if (e = void 0 !== e ? e : {}, !isSupported()) return void console.log(new SupportError);
const n = [
- [Accordion, t.accordion],
- [Button, t.button],
- [CharacterCount, t.characterCount],
+ [Accordion, e.accordion],
+ [Button, e.button],
+ [CharacterCount, e.characterCount],
[Checkboxes],
- [ErrorSummary, t.errorSummary],
- [ExitThisPage, t.exitThisPage],
+ [ErrorSummary, e.errorSummary],
+ [ExitThisPage, e.exitThisPage],
[Header],
- [NotificationBanner, t.notificationBanner],
+ [NotificationBanner, e.notificationBanner],
[Radios],
[SkipLink],
[Tabs]
],
- i = null != (e = t.scope) ? e : document;
- n.forEach((([t, e]) => {
- i.querySelectorAll(`[data-module="${t.moduleName}"]`).forEach((n => {
+ i = null != (t = e.scope) ? t : document;
+ n.forEach((([e, t]) => {
+ i.querySelectorAll(`[data-module="${e.moduleName}"]`).forEach((n => {
try {
- "defaults" in t ? new t(n, e) : new t(n)
+ "defaults" in e ? new e(n, t) : new e(n)
} catch (i) {
console.log(i)
}
Action run for 8ce1dc4 |
Other changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index 319a2fe70..a83698cb6 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -6,44 +6,75 @@
const version = 'development';
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
- function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function getFragmentFromUrl(url) {
if (!url.includes('#')) {
@@ -93,28 +124,44 @@
const validationErrors = [];
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
- for (const {
- required,
- errorMessage
- } of conditions) {
- if (!required.every(key => !!config[key])) {
- errors.push(errorMessage);
+ if (Array.isArray(conditions)) {
+ for (const {
+ required,
+ errorMessage
+ } of conditions) {
+ if (!required.every(key => !!config[key])) {
+ errors.push(errorMessage);
+ }
+ }
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
+ validationErrors.push(...errors);
}
- }
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
- validationErrors.push(...errors);
}
}
return validationErrors;
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -123,26 +170,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -212,18 +248,21 @@
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -251,12 +290,15 @@
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -443,8 +485,8 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
throw new ElementError({
@@ -680,6 +722,16 @@
},
rememberExpanded: true
});
+ Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ rememberExpanded: {
+ type: 'boolean'
+ }
+ }
+ });
const helper = {
/**
* Check for `window.sessionStorage`, and that it actually works.
@@ -733,6 +785,10 @@
* 'Show' button's accessible name when a section is expanded.
*/
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
+
const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
/**
@@ -758,7 +814,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
this.$module.addEventListener('click', event => this.debounce(event));
}
@@ -793,10 +849,21 @@
* @property {boolean} [preventDoubleClick=false] - Prevent accidental double
* clicks on submit buttons from submitting forms multiple times.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
Button.moduleName = 'govuk-button';
Button.defaults = Object.freeze({
preventDoubleClick: false
});
+ Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: 'boolean'
+ }
+ }
+ });
function closestAttributeValue($element, attributeName) {
const $closestElementWithAttribute = $element.closest(`[${attributeName}]`);
@@ -849,7 +916,7 @@
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset($module.dataset);
+ const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
let configOverrides = {};
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
@@ -862,7 +929,7 @@
if (errors[0]) {
throw new ConfigError(`Character count: ${errors[0]}`);
}
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
+ this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue($module, 'lang')
});
this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
@@ -1075,6 +1142,20 @@
}
});
CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ maxwords: {
+ type: 'number'
+ },
+ maxlength: {
+ type: 'number'
+ },
+ threshold: {
+ type: 'number'
+ }
+ },
anyOf: [{
required: ['maxwords'],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
@@ -1224,7 +1305,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
if (!this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -1289,10 +1370,21 @@
* @property {boolean} [disableAutoFocus=false] - If set to `true` the error
* summary will not be focussed when the page loads.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ErrorSummary.moduleName = 'govuk-error-summary';
ErrorSummary.defaults = Object.freeze({
disableAutoFocus: false
});
+ ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+ });
/**
* Exit this page component
@@ -1335,8 +1427,8 @@
identifier: 'Button (`.govuk-exit-this-page__button`)'
});
}
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
this.$module = $module;
this.$button = $button;
const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -1500,6 +1592,10 @@
* @property {string} [pressOneMoreTime] - Screen reader announcement informing
* the user they must press the activation key one more time.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ExitThisPage.moduleName = 'govuk-exit-this-page';
ExitThisPage.defaults = Object.freeze({
i18n: {
@@ -1509,6 +1605,13 @@
pressOneMoreTime: 'Shift, press 1 more time to exit.'
}
});
+ ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
/**
* Header component
@@ -1623,7 +1726,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -1639,10 +1742,21 @@
* applies if the component has a `role` of `alert` – in other cases the
* component will not be focused on page load, regardless of this option.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
NotificationBanner.moduleName = 'govuk-notification-banner';
NotificationBanner.defaults = Object.freeze({
disableAutoFocus: false
});
+ NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+ });
/**
* Radios component
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 1e8dbc738..7662df517 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1,43 +1,74 @@
const version = 'development';
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
-function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function getFragmentFromUrl(url) {
if (!url.includes('#')) {
@@ -87,28 +118,44 @@ function validateConfig(schema, config) {
const validationErrors = [];
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
- for (const {
- required,
- errorMessage
- } of conditions) {
- if (!required.every(key => !!config[key])) {
- errors.push(errorMessage);
+ if (Array.isArray(conditions)) {
+ for (const {
+ required,
+ errorMessage
+ } of conditions) {
+ if (!required.every(key => !!config[key])) {
+ errors.push(errorMessage);
+ }
+ }
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
+ validationErrors.push(...errors);
}
- }
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
- validationErrors.push(...errors);
}
}
return validationErrors;
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -117,26 +164,15 @@ function validateConfig(schema, config) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -206,18 +242,21 @@ class I18n {
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -245,12 +284,15 @@ class I18n {
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -437,8 +479,8 @@ class Accordion extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
throw new ElementError({
@@ -674,6 +716,16 @@ Accordion.defaults = Object.freeze({
},
rememberExpanded: true
});
+Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ rememberExpanded: {
+ type: 'boolean'
+ }
+ }
+});
const helper = {
/**
* Check for `window.sessionStorage`, and that it actually works.
@@ -727,6 +779,10 @@ const helper = {
* 'Show' button's accessible name when a section is expanded.
*/
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
+
const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
/**
@@ -752,7 +808,7 @@ class Button extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
this.$module.addEventListener('click', event => this.debounce(event));
}
@@ -787,10 +843,21 @@ class Button extends GOVUKFrontendComponent {
* @property {boolean} [preventDoubleClick=false] - Prevent accidental double
* clicks on submit buttons from submitting forms multiple times.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
Button.moduleName = 'govuk-button';
Button.defaults = Object.freeze({
preventDoubleClick: false
});
+Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: 'boolean'
+ }
+ }
+});
function closestAttributeValue($element, attributeName) {
const $closestElementWithAttribute = $element.closest(`[${attributeName}]`);
@@ -843,7 +910,7 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset($module.dataset);
+ const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
let configOverrides = {};
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
@@ -856,7 +923,7 @@ class CharacterCount extends GOVUKFrontendComponent {
if (errors[0]) {
throw new ConfigError(`Character count: ${errors[0]}`);
}
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
+ this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue($module, 'lang')
});
this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
@@ -1069,6 +1136,20 @@ CharacterCount.defaults = Object.freeze({
}
});
CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ maxwords: {
+ type: 'number'
+ },
+ maxlength: {
+ type: 'number'
+ },
+ threshold: {
+ type: 'number'
+ }
+ },
anyOf: [{
required: ['maxwords'],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
@@ -1218,7 +1299,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
if (!this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -1283,10 +1364,21 @@ class ErrorSummary extends GOVUKFrontendComponent {
* @property {boolean} [disableAutoFocus=false] - If set to `true` the error
* summary will not be focussed when the page loads.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ErrorSummary.moduleName = 'govuk-error-summary';
ErrorSummary.defaults = Object.freeze({
disableAutoFocus: false
});
+ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
/**
* Exit this page component
@@ -1329,8 +1421,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
identifier: 'Button (`.govuk-exit-this-page__button`)'
});
}
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
this.$module = $module;
this.$button = $button;
const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -1494,6 +1586,10 @@ class ExitThisPage extends GOVUKFrontendComponent {
* @property {string} [pressOneMoreTime] - Screen reader announcement informing
* the user they must press the activation key one more time.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ExitThisPage.moduleName = 'govuk-exit-this-page';
ExitThisPage.defaults = Object.freeze({
i18n: {
@@ -1503,6 +1599,13 @@ ExitThisPage.defaults = Object.freeze({
pressOneMoreTime: 'Shift, press 1 more time to exit.'
}
});
+ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
/**
* Header component
@@ -1617,7 +1720,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -1633,10 +1736,21 @@ class NotificationBanner extends GOVUKFrontendComponent {
* applies if the component has a `role` of `alert` – in other cases the
* component will not be focused on page load, regardless of this option.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
NotificationBanner.moduleName = 'govuk-notification-banner';
NotificationBanner.defaults = Object.freeze({
disableAutoFocus: false
});
+NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
/**
* Radios component
diff --git a/packages/govuk-frontend/dist/govuk/common/index.mjs b/packages/govuk-frontend/dist/govuk/common/index.mjs
index 1d272c1ba..8b1e29b27 100644
--- a/packages/govuk-frontend/dist/govuk/common/index.mjs
+++ b/packages/govuk-frontend/dist/govuk/common/index.mjs
@@ -1,41 +1,45 @@
+import { normaliseString } from './normalise-string.mjs';
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
-function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function getFragmentFromUrl(url) {
if (!url.includes('#')) {
@@ -85,28 +89,44 @@ function validateConfig(schema, config) {
const validationErrors = [];
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
- for (const {
- required,
- errorMessage
- } of conditions) {
- if (!required.every(key => !!config[key])) {
- errors.push(errorMessage);
+ if (Array.isArray(conditions)) {
+ for (const {
+ required,
+ errorMessage
+ } of conditions) {
+ if (!required.every(key => !!config[key])) {
+ errors.push(errorMessage);
+ }
+ }
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
+ validationErrors.push(...errors);
}
- }
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
- validationErrors.push(...errors);
}
}
return validationErrors;
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/common/normalise-dataset.mjs b/packages/govuk-frontend/dist/govuk/common/normalise-dataset.mjs
index f75acc88d..8ca07860e 100644
--- a/packages/govuk-frontend/dist/govuk/common/normalise-dataset.mjs
+++ b/packages/govuk-frontend/dist/govuk/common/normalise-dataset.mjs
@@ -1,26 +1,18 @@
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+import { extractConfigByNamespace } from './index.mjs';
+import { normaliseString } from './normalise-string.mjs';
+
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
-export { normaliseDataset, normaliseString };
+export { normaliseDataset };
//# sourceMappingURL=normalise-dataset.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/common/normalise-string.mjs b/packages/govuk-frontend/dist/govuk/common/normalise-string.mjs
new file mode 100644
index 000000000..e0891c04e
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/common/normalise-string.mjs
@@ -0,0 +1,31 @@
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+export { normaliseString };
+//# sourceMappingURL=normalise-string.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
index d5dca973f..aa392371c 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -4,44 +4,75 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
})(this, (function (exports) { 'use strict';
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
- function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -49,14 +80,28 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -65,26 +110,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -148,18 +182,21 @@
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -187,12 +224,15 @@
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -379,8 +419,8 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
throw new ElementError({
@@ -616,6 +656,16 @@
},
rememberExpanded: true
});
+ Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ rememberExpanded: {
+ type: 'boolean'
+ }
+ }
+ });
const helper = {
/**
* Check for `window.sessionStorage`, and that it actually works.
@@ -669,6 +719,10 @@
* 'Show' button's accessible name when a section is expanded.
*/
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
+
exports.Accordion = Accordion;
}));
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
index eecebe24c..74fcadec2 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -1,41 +1,72 @@
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
-function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -43,14 +74,28 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -59,26 +104,15 @@ function isSupported($scope = document.body) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -142,18 +176,21 @@ class I18n {
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -181,12 +218,15 @@ class I18n {
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -373,8 +413,8 @@ class Accordion extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
throw new ElementError({
@@ -610,6 +650,16 @@ Accordion.defaults = Object.freeze({
},
rememberExpanded: true
});
+Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ rememberExpanded: {
+ type: 'boolean'
+ }
+ }
+});
const helper = {
/**
* Check for `window.sessionStorage`, and that it actually works.
@@ -663,5 +713,9 @@ const helper = {
* 'Show' button's accessible name when a section is expanded.
*/
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
+
export { Accordion };
//# sourceMappingURL=accordion.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
index 1ccea5293..1c57601ca 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
@@ -1,4 +1,4 @@
-import { mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs';
+import { mergeConfigs } from '../../common/index.mjs';
import { normaliseDataset } from '../../common/normalise-dataset.mjs';
import { ElementError } from '../../errors/index.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
@@ -60,8 +60,8 @@ class Accordion extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
throw new ElementError({
@@ -297,6 +297,16 @@ Accordion.defaults = Object.freeze({
},
rememberExpanded: true
});
+Accordion.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ rememberExpanded: {
+ type: 'boolean'
+ }
+ }
+});
const helper = {
/**
* Check for `window.sessionStorage`, and that it actually works.
@@ -350,5 +360,9 @@ const helper = {
* 'Show' button's accessible name when a section is expanded.
*/
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
+
export { Accordion };
//# sourceMappingURL=accordion.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
index e8db70624..d67071a8f 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
@@ -4,45 +4,104 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
})(this, (function (exports) { 'use strict';
- function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
}
- flattenLoop(configObject);
- return flattenedObject;
}
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+ function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+ }
function isSupported($scope = document.body) {
if (!$scope) {
return false;
}
return $scope.classList.contains('govuk-frontend-supported');
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -51,26 +110,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -147,7 +195,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
this.$module.addEventListener('click', event => this.debounce(event));
}
@@ -182,10 +230,21 @@
* @property {boolean} [preventDoubleClick=false] - Prevent accidental double
* clicks on submit buttons from submitting forms multiple times.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
Button.moduleName = 'govuk-button';
Button.defaults = Object.freeze({
preventDoubleClick: false
});
+ Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: 'boolean'
+ }
+ }
+ });
exports.Button = Button;
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
index 5e00ee6d9..38af24c35 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
@@ -1,42 +1,101 @@
-function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
}
- flattenLoop(configObject);
- return flattenedObject;
}
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+}
function isSupported($scope = document.body) {
if (!$scope) {
return false;
}
return $scope.classList.contains('govuk-frontend-supported');
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -45,26 +104,15 @@ function isSupported($scope = document.body) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -141,7 +189,7 @@ class Button extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
this.$module.addEventListener('click', event => this.debounce(event));
}
@@ -176,10 +224,21 @@ class Button extends GOVUKFrontendComponent {
* @property {boolean} [preventDoubleClick=false] - Prevent accidental double
* clicks on submit buttons from submitting forms multiple times.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
Button.moduleName = 'govuk-button';
Button.defaults = Object.freeze({
preventDoubleClick: false
});
+Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: 'boolean'
+ }
+ }
+});
export { Button };
//# sourceMappingURL=button.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
index 541e56fab..db2683005 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
@@ -28,7 +28,7 @@ class Button extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
this.$module.addEventListener('click', event => this.debounce(event));
}
@@ -63,10 +63,21 @@ class Button extends GOVUKFrontendComponent {
* @property {boolean} [preventDoubleClick=false] - Prevent accidental double
* clicks on submit buttons from submitting forms multiple times.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
Button.moduleName = 'govuk-button';
Button.defaults = Object.freeze({
preventDoubleClick: false
});
+Button.schema = Object.freeze({
+ properties: {
+ preventDoubleClick: {
+ type: 'boolean'
+ }
+ }
+});
export { Button };
//# sourceMappingURL=button.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
index 482acde8d..baeeae62c 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
@@ -9,44 +9,75 @@
return $closestElementWithAttribute ? $closestElementWithAttribute.getAttribute(attributeName) : null;
}
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
- function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -58,28 +89,44 @@
const validationErrors = [];
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
- for (const {
- required,
- errorMessage
- } of conditions) {
- if (!required.every(key => !!config[key])) {
- errors.push(errorMessage);
+ if (Array.isArray(conditions)) {
+ for (const {
+ required,
+ errorMessage
+ } of conditions) {
+ if (!required.every(key => !!config[key])) {
+ errors.push(errorMessage);
+ }
+ }
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
+ validationErrors.push(...errors);
}
- }
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
- validationErrors.push(...errors);
}
}
return validationErrors;
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -88,26 +135,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -177,18 +213,21 @@
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -216,12 +255,15 @@
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -398,7 +440,7 @@
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset($module.dataset);
+ const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
let configOverrides = {};
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
@@ -411,7 +453,7 @@
if (errors[0]) {
throw new ConfigError(`Character count: ${errors[0]}`);
}
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
+ this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue($module, 'lang')
});
this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
@@ -624,6 +666,20 @@
}
});
CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ maxwords: {
+ type: 'number'
+ },
+ maxlength: {
+ type: 'number'
+ },
+ threshold: {
+ type: 'number'
+ }
+ },
anyOf: [{
required: ['maxwords'],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
index 8fcb8512d..473db45b5 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
@@ -3,44 +3,75 @@ function closestAttributeValue($element, attributeName) {
return $closestElementWithAttribute ? $closestElementWithAttribute.getAttribute(attributeName) : null;
}
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
-function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -52,28 +83,44 @@ function validateConfig(schema, config) {
const validationErrors = [];
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
- for (const {
- required,
- errorMessage
- } of conditions) {
- if (!required.every(key => !!config[key])) {
- errors.push(errorMessage);
+ if (Array.isArray(conditions)) {
+ for (const {
+ required,
+ errorMessage
+ } of conditions) {
+ if (!required.every(key => !!config[key])) {
+ errors.push(errorMessage);
+ }
+ }
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
+ validationErrors.push(...errors);
}
- }
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
- validationErrors.push(...errors);
}
}
return validationErrors;
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -82,26 +129,15 @@ function validateConfig(schema, config) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -171,18 +207,21 @@ class I18n {
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -210,12 +249,15 @@ class I18n {
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -392,7 +434,7 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset($module.dataset);
+ const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
let configOverrides = {};
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
@@ -405,7 +447,7 @@ class CharacterCount extends GOVUKFrontendComponent {
if (errors[0]) {
throw new ConfigError(`Character count: ${errors[0]}`);
}
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
+ this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue($module, 'lang')
});
this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
@@ -618,6 +660,20 @@ CharacterCount.defaults = Object.freeze({
}
});
CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ maxwords: {
+ type: 'number'
+ },
+ maxlength: {
+ type: 'number'
+ },
+ threshold: {
+ type: 'number'
+ }
+ },
anyOf: [{
required: ['maxwords'],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
index 953feb6f5..955e4ef16 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
@@ -1,5 +1,5 @@
import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';
-import { mergeConfigs, validateConfig, extractConfigByNamespace } from '../../common/index.mjs';
+import { mergeConfigs, validateConfig } from '../../common/index.mjs';
import { normaliseDataset } from '../../common/normalise-dataset.mjs';
import { ElementError, ConfigError } from '../../errors/index.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
@@ -51,7 +51,7 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset($module.dataset);
+ const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
let configOverrides = {};
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
@@ -64,7 +64,7 @@ class CharacterCount extends GOVUKFrontendComponent {
if (errors[0]) {
throw new ConfigError(`Character count: ${errors[0]}`);
}
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
+ this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue($module, 'lang')
});
this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
@@ -277,6 +277,20 @@ CharacterCount.defaults = Object.freeze({
}
});
CharacterCount.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ },
+ maxwords: {
+ type: 'number'
+ },
+ maxlength: {
+ type: 'number'
+ },
+ threshold: {
+ type: 'number'
+ }
+ },
anyOf: [{
required: ['maxwords'],
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
index cd1ad83e7..e8fb370f5 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
@@ -51,9 +51,17 @@
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
index e526485d6..8e414cdba 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
@@ -45,9 +45,17 @@ function isSupported($scope = document.body) {
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
index 649a5eb30..32f091022 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
@@ -4,31 +4,76 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
})(this, (function (exports) { 'use strict';
- function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
}
- flattenLoop(configObject);
- return flattenedObject;
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
}
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+ function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+ }
function getFragmentFromUrl(url) {
if (!url.includes('#')) {
return undefined;
@@ -65,14 +110,28 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -81,26 +140,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -177,7 +225,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
if (!this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -242,10 +290,21 @@
* @property {boolean} [disableAutoFocus=false] - If set to `true` the error
* summary will not be focussed when the page loads.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ErrorSummary.moduleName = 'govuk-error-summary';
ErrorSummary.defaults = Object.freeze({
disableAutoFocus: false
});
+ ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+ });
exports.ErrorSummary = ErrorSummary;
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
index 02bf81818..900032a18 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
@@ -1,28 +1,73 @@
-function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
}
- flattenLoop(configObject);
- return flattenedObject;
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
}
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+}
function getFragmentFromUrl(url) {
if (!url.includes('#')) {
return undefined;
@@ -59,14 +104,28 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -75,26 +134,15 @@ function isSupported($scope = document.body) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -171,7 +219,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
if (!this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -236,10 +284,21 @@ class ErrorSummary extends GOVUKFrontendComponent {
* @property {boolean} [disableAutoFocus=false] - If set to `true` the error
* summary will not be focussed when the page loads.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ErrorSummary.moduleName = 'govuk-error-summary';
ErrorSummary.defaults = Object.freeze({
disableAutoFocus: false
});
+ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
export { ErrorSummary };
//# sourceMappingURL=error-summary.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
index f0e343e1b..1fd0f166e 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
@@ -28,7 +28,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
if (!this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -93,10 +93,21 @@ class ErrorSummary extends GOVUKFrontendComponent {
* @property {boolean} [disableAutoFocus=false] - If set to `true` the error
* summary will not be focussed when the page loads.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ErrorSummary.moduleName = 'govuk-error-summary';
ErrorSummary.defaults = Object.freeze({
disableAutoFocus: false
});
+ErrorSummary.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
export { ErrorSummary };
//# sourceMappingURL=error-summary.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
index 4d726b847..bf0fc9157 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
@@ -4,44 +4,75 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
})(this, (function (exports) { 'use strict';
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
- function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -49,14 +80,28 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -65,26 +110,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -148,18 +182,21 @@
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -187,12 +224,15 @@
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -364,8 +404,8 @@
identifier: 'Button (`.govuk-exit-this-page__button`)'
});
}
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
this.$module = $module;
this.$button = $button;
const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -529,6 +569,10 @@
* @property {string} [pressOneMoreTime] - Screen reader announcement informing
* the user they must press the activation key one more time.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ExitThisPage.moduleName = 'govuk-exit-this-page';
ExitThisPage.defaults = Object.freeze({
i18n: {
@@ -538,6 +582,13 @@
pressOneMoreTime: 'Shift, press 1 more time to exit.'
}
});
+ ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
exports.ExitThisPage = ExitThisPage;
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
index a8e072477..34b812e2e 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
@@ -1,41 +1,72 @@
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
- }
- flattenLoop(configObject);
- return flattenedObject;
- }
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
-function extractConfigByNamespace(configObject, namespace) {
- const newObject = {};
- for (const [key, value] of Object.entries(configObject)) {
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
const keyParts = key.split('.');
- if (keyParts[0] === namespace) {
- if (keyParts.length > 1) {
- keyParts.shift();
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
}
- const newKey = keyParts.join('.');
- newObject[newKey] = value;
}
}
- return newObject;
+ return newObject[namespace];
}
function isSupported($scope = document.body) {
if (!$scope) {
@@ -43,14 +74,28 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -59,26 +104,15 @@ function isSupported($scope = document.body) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -142,18 +176,21 @@ class I18n {
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -181,12 +218,15 @@ class I18n {
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
@@ -358,8 +398,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
identifier: 'Button (`.govuk-exit-this-page__button`)'
});
}
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
this.$module = $module;
this.$button = $button;
const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -523,6 +563,10 @@ class ExitThisPage extends GOVUKFrontendComponent {
* @property {string} [pressOneMoreTime] - Screen reader announcement informing
* the user they must press the activation key one more time.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ExitThisPage.moduleName = 'govuk-exit-this-page';
ExitThisPage.defaults = Object.freeze({
i18n: {
@@ -532,6 +576,13 @@ ExitThisPage.defaults = Object.freeze({
pressOneMoreTime: 'Shift, press 1 more time to exit.'
}
});
+ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
export { ExitThisPage };
//# sourceMappingURL=exit-this-page.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
index fa157c93e..b4cd38985 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
@@ -1,4 +1,4 @@
-import { mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs';
+import { mergeConfigs } from '../../common/index.mjs';
import { normaliseDataset } from '../../common/normalise-dataset.mjs';
import { ElementError } from '../../errors/index.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
@@ -45,8 +45,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
identifier: 'Button (`.govuk-exit-this-page__button`)'
});
}
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset($module.dataset));
- this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
+ this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
+ this.i18n = new I18n(this.config.i18n);
this.$module = $module;
this.$button = $button;
const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -210,6 +210,10 @@ class ExitThisPage extends GOVUKFrontendComponent {
* @property {string} [pressOneMoreTime] - Screen reader announcement informing
* the user they must press the activation key one more time.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
ExitThisPage.moduleName = 'govuk-exit-this-page';
ExitThisPage.defaults = Object.freeze({
i18n: {
@@ -219,6 +223,13 @@ ExitThisPage.defaults = Object.freeze({
pressOneMoreTime: 'Shift, press 1 more time to exit.'
}
});
+ExitThisPage.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
export { ExitThisPage };
//# sourceMappingURL=exit-this-page.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
index ee3b1e1c6..aa732880f 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
@@ -23,9 +23,17 @@
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
index 98e82f86d..14d66876b 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
@@ -17,9 +17,17 @@ function isSupported($scope = document.body) {
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
index dbe944a6a..e1450e006 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
@@ -4,31 +4,76 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
})(this, (function (exports) { 'use strict';
- function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
}
- flattenLoop(configObject);
- return flattenedObject;
}
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+ function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+ }
function setFocus($element, options = {}) {
var _options$onBeforeFocu;
const isFocusable = $element.getAttribute('tabindex');
@@ -59,14 +104,28 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -75,26 +134,15 @@
* @property {string} errorMessage - Error message when required config fields not provided
*/
- function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
- }
- function normaliseDataset(dataset) {
+ function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -168,7 +216,7 @@
});
}
this.$module = $module;
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -184,10 +232,21 @@
* applies if the component has a `role` of `alert` – in other cases the
* component will not be focused on page load, regardless of this option.
*/
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
NotificationBanner.moduleName = 'govuk-notification-banner';
NotificationBanner.defaults = Object.freeze({
disableAutoFocus: false
});
+ NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+ });
exports.NotificationBanner = NotificationBanner;
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
index f0fcf2532..b9bddd534 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
@@ -1,28 +1,73 @@
-function mergeConfigs(...configObjects) {
- function flattenObject(configObject) {
- const flattenedObject = {};
- function flattenLoop(obj, prefix) {
- for (const [key, value] of Object.entries(obj)) {
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object') {
- flattenLoop(value, prefixedKey);
- } else {
- flattenedObject[prefixedKey] = value;
- }
- }
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
}
- flattenLoop(configObject);
- return flattenedObject;
}
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+function mergeConfigs(...configObjects) {
const formattedConfigObject = {};
for (const configObject of configObjects) {
- const obj = flattenObject(configObject);
- for (const [key, value] of Object.entries(obj)) {
- formattedConfigObject[key] = value;
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
}
}
return formattedConfigObject;
}
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+}
function setFocus($element, options = {}) {
var _options$onBeforeFocu;
const isFocusable = $element.getAttribute('tabindex');
@@ -53,14 +98,28 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
/**
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
@@ -69,26 +128,15 @@ function isSupported($scope = document.body) {
* @property {string} errorMessage - Error message when required config fields not provided
*/
-function normaliseString(value) {
- if (typeof value !== 'string') {
- return value;
- }
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- return true;
- }
- if (trimmedValue === 'false') {
- return false;
- }
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
- return Number(trimmedValue);
- }
- return value;
-}
-function normaliseDataset(dataset) {
+function normaliseDataset(Component, dataset) {
const out = {};
- for (const [key, value] of Object.entries(dataset)) {
- out[key] = normaliseString(value);
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
}
return out;
}
@@ -162,7 +210,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -178,10 +226,21 @@ class NotificationBanner extends GOVUKFrontendComponent {
* applies if the component has a `role` of `alert` – in other cases the
* component will not be focused on page load, regardless of this option.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
NotificationBanner.moduleName = 'govuk-notification-banner';
NotificationBanner.defaults = Object.freeze({
disableAutoFocus: false
});
+NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
export { NotificationBanner };
//# sourceMappingURL=notification-banner.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
index 1af8f3d96..1ef1621bc 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
@@ -25,7 +25,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
});
}
this.$module = $module;
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset($module.dataset));
+ this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
setFocus(this.$module);
}
@@ -41,10 +41,21 @@ class NotificationBanner extends GOVUKFrontendComponent {
* applies if the component has a `role` of `alert` – in other cases the
* component will not be focused on page load, regardless of this option.
*/
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ */
NotificationBanner.moduleName = 'govuk-notification-banner';
NotificationBanner.defaults = Object.freeze({
disableAutoFocus: false
});
+NotificationBanner.schema = Object.freeze({
+ properties: {
+ disableAutoFocus: {
+ type: 'boolean'
+ }
+ }
+});
export { NotificationBanner };
//# sourceMappingURL=notification-banner.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
index 11cb7cbe4..4ade86db5 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
@@ -51,9 +51,17 @@
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
index 076d273b2..fcaf82356 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
@@ -45,9 +45,17 @@ function isSupported($scope = document.body) {
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
index 03f8c198f..1e8879f6b 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
@@ -45,9 +45,17 @@
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
index 8386205c1..0b6822202 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
@@ -39,9 +39,17 @@ function isSupported($scope = document.body) {
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
index 8b0bdb9c0..63cce73bc 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
@@ -29,9 +29,17 @@
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
index db1378320..01efd7654 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
@@ -23,9 +23,17 @@ function isSupported($scope = document.body) {
* Schema for component config
*
* @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
* @property {SchemaCondition[]} [anyOf] - List of schema conditions
*/
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
/**
* Schema condition for component config
*
diff --git a/packages/govuk-frontend/dist/govuk/i18n.mjs b/packages/govuk-frontend/dist/govuk/i18n.mjs
index 3ea5259bb..f7a76fb04 100644
--- a/packages/govuk-frontend/dist/govuk/i18n.mjs
+++ b/packages/govuk-frontend/dist/govuk/i18n.mjs
@@ -10,18 +10,21 @@ class I18n {
if (!lookupKey) {
throw new Error('i18n: lookup key missing');
}
- if (typeof (options == null ? void 0 : options.count) === 'number') {
- lookupKey = `${lookupKey}.${this.getPluralSuffix(lookupKey, options.count)}`;
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
}
- const translationString = this.translations[lookupKey];
- if (typeof translationString === 'string') {
- if (translationString.match(/%{(.\S+)}/)) {
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
if (!options) {
throw new Error('i18n: cannot replace placeholders in string if no option data provided');
}
- return this.replacePlaceholders(translationString, options);
+ return this.replacePlaceholders(translation, options);
}
- return translationString;
+ return translation;
}
return lookupKey;
}
@@ -49,12 +52,15 @@ class I18n {
if (!isFinite(count)) {
return 'other';
}
+ const translation = this.translations[lookupKey];
const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
- if (`${lookupKey}.${preferredForm}` in this.translations) {
- return preferredForm;
- } else if (`${lookupKey}.other` in this.translations) {
- console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
- return 'other';
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
}
Action run for 8ce1dc4 |
7e65168
to
de0937d
Compare
de0937d
to
437c982
Compare
A little thought raise by the following line in #4971:
With the move to nested configs rather than flattened, are we at risk of:
Should we ignore empty values when merging, maybe? |
@romaricpascal Ah we kept this as a feature though E.g. Character Count with Update: See tests in 48213c0 for those at risk scenarios |
437c982
to
ca4d2c0
Compare
ca4d2c0
to
4a77ad8
Compare
This configured allowed types for config fields so we can check them in future
This prevents circular dependency issues
4a77ad8
to
2c822ac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like the direction, and I think we can sort the slight difference of behaviour with the flattened config in a separate PR (with the non-flattened one, shallower keys – when set –, override deeper ones, while this doesn't happen in the flattened one). Left a few questions/comments, but I don't think it's anything major (hopefully) 😊
2c822ac
to
c9fa72a
Compare
@@ -86,7 +86,7 @@ export function extractConfigByNamespace(Component, dataset, namespace) { | |||
// Drop down to what we assume is a nested object | |||
// but check for object type on the next loop | |||
if (index < keyParts.length - 1) { | |||
current = current[name] = current[name] ?? {} | |||
current = current[name] = isObject(current[name]) ? current[name] : {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No issue with the change of logic, but the commit attracted my attention to this line. I think it's compact but a bit tricky to follow. Can it be split into the following (which if I understood wrong further highlights the need to separate the statements 😆 )?
current = current[name] = isObject(current[name]) ? current[name] : {} | |
current[name] = isObject(current[name]) ? current[name] : {} | |
current = current[name] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@romaricpascal Found the perfect ESLint rule for you no-multi-assign
Will do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh dreamy, thanks for pointing me to it. I'll raise a separate PR to enable it (in case there's a couple multi-assign in the rest of the code).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 21bc20b with extra space for comments too
fa0f75f
to
ccd7f2f
Compare
ccd7f2f
to
4a40d63
Compare
@romaricpascal last fixes are now in Thanks for helping with the new tests in 4a40d63
The only behaviour differences I can see are probably fixes we want to keep Can you double check? Behaviour differencesComparing the new tests in 4a40d63 (nested configs) with 57391be (flat configs) shows we've resolved: Bug 1: Stray dot-separated values were not discardedFor example <div id="app-example"
data-f.l="elk"
data-f.l.e="elephant">
</div> govuk-frontend/packages/govuk-frontend/src/govuk/common/index.jsdom.test.mjs Lines 358 to 363 in ca666b8
Bug 2: String values were hoisted into objects merged over themFor example <div id="app-example"
data-c="jellyfish"
data-c.a="cat"
data-c.o="cow">
</div> govuk-frontend/packages/govuk-frontend/src/govuk/common/index.jsdom.test.mjs Lines 286 to 291 in ca666b8
Bug 3: Objects merged over string values preserved the hoisted value, not the object valueFor example <div id="app-example"
data-c.c="ccrow"
data-c="jellyfish"
data-c.a="cat"
data-c.o="cow">
</div> govuk-frontend/packages/govuk-frontend/src/govuk/common/index.jsdom.test.mjs Lines 312 to 316 in ca666b8
|
@romaricpascal I should flip the params of these two to match? normaliseDataset($module.dataset, Accordion)
extractConfigByNamespace(Accordion, $module.dataset, 'i18n') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to go for me! It's nice to have everything as objects in the config now.
@colinrotherham That sounds a sensible change so that both calls are consistent. Not blocking though if time is short. |
Since we only pass in `DOMStringMap` values, this removes all tests for `normaliseDataset()` that don’t pass in strings We also prevent unnecessary data attributes (e.g. `data-module="govuk-accordion"’) from being merged into the config See: #4230
We can reduce the code we use by expanding dot-separated data-attributes at the point we read them This lets us remove `flattenConfigs()` since we can use nested configs everywhere
Do you want a `data-example="2024"` to stay as a string? Data attributes now understand component config schema types Extra checks added to guard against objects, arrays and non-finite numbers (NaN, Infinity)
…allow keys are set
4a40d63
to
8ce1dc4
Compare
Switch to nested (not flattened) configs with stricter checks
Closes #4230 to fix known issues flagged again during Password input testing in:
Config snags
"2024"
or"false"
become number/boolean (even if you want strings)Further reading
Example with custom attributes
Configuring Password input with
formGroup.attributes
Before
Merged component config options are flattened and include other
formGroup.attributes
E.g. Password input
this.config
showsmodule
,sneaky
andsneaky2
options:After
Merged component config options are nested and match the schema