-
Notifications
You must be signed in to change notification settings - Fork 326
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
Spike into Config
class if defined by child class of GOVUKFrontendComponent
#5427
base: main
Are you sure you want to change the base?
Conversation
📋 StatsFile sizes
Modules
View stats and visualisations on the review app Action run for 629d6ee |
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 0416a210b..0efb65c6b 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1,44 +1,44 @@
const version = "development";
function normaliseString(t, e) {
- const s = t ? t.trim() : "";
- let n, i = null == e ? void 0 : e.type;
- switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
+ const n = t ? t.trim() : "";
+ let i, s = null == e ? void 0 : e.type;
+ switch (s || (["true", "false"].includes(n) && (s = "boolean"), n.length > 0 && isFinite(Number(n)) && (s = "number")), s) {
case "boolean":
- n = "true" === s;
+ i = "true" === n;
break;
case "number":
- n = Number(s);
+ i = Number(n);
break;
default:
- n = t
+ i = t
}
- return n
+ return i
}
function mergeConfigs(...t) {
const e = {};
- for (const s of t)
- for (const t of Object.keys(s)) {
- const n = e[t],
- i = s[t];
- isObject(n) && isObject(i) ? e[t] = mergeConfigs(n, i) : e[t] = i
+ for (const n of t)
+ for (const t of Object.keys(n)) {
+ const i = e[t],
+ s = n[t];
+ isObject(i) && isObject(s) ? e[t] = mergeConfigs(i, s) : e[t] = s
}
return e
}
function extractConfigByNamespace(Component, t, e) {
- const s = Component.schema.properties[e];
- if ("object" !== (null == s ? void 0 : s.type)) return;
- const n = {
+ const n = Component.schema.properties[e];
+ if ("object" !== (null == n ? void 0 : n.type)) return;
+ const i = {
[e]: {}
};
- for (const [i, o] of Object.entries(t)) {
- let t = n;
- const s = i.split(".");
- for (const [n, r] of s.entries()) "object" == typeof t && (n < s.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : i !== e && (t[r] = normaliseString(o)))
+ for (const [s, o] of Object.entries(t)) {
+ let t = i;
+ const n = s.split(".");
+ for (const [i, r] of n.entries()) "object" == typeof t && (i < n.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : s !== e && (t[r] = normaliseString(o)))
}
- return n[e]
+ return i[e]
}
function getFragmentFromUrl(t) {
@@ -54,20 +54,20 @@ function getBreakpoint(t) {
}
function setFocus(t, e = {}) {
- var s;
- const n = t.getAttribute("tabindex");
+ var n;
+ const i = t.getAttribute("tabindex");
function onBlur() {
- var s;
- null == (s = e.onBlur) || s.call(t), n || t.removeAttribute("tabindex")
+ var n;
+ null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
}
- n || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+ i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
t.addEventListener("blur", onBlur, {
once: !0
})
}), {
once: !0
- }), null == (s = e.onBeforeFocus) || s.call(t), t.focus()
+ }), null == (n = e.onBeforeFocus) || n.call(t), t.focus()
}
function isSupported(t = document.body) {
@@ -83,12 +83,6 @@ function isObject(t) {
function formatErrorMessage(Component, t) {
return `${Component.moduleName}: ${t}`
}
-
-function normaliseDataset(Component, t) {
- const e = {};
- for (const [s, n] of Object.entries(Component.schema.properties)) s in t && (e[s] = normaliseString(t[s], n)), "object" === (null == n ? void 0 : n.type) && (e[s] = extractConfigByNamespace(Component, t, s));
- return e
-}
class GOVUKFrontendError extends Error {
constructor(...t) {
super(...t), this.name = "GOVUKFrontendError"
@@ -110,12 +104,12 @@ class ElementError extends GOVUKFrontendError {
let e = "string" == typeof t ? t : "";
if ("object" == typeof t) {
const {
- component: s,
- identifier: n,
- element: i,
+ component: n,
+ identifier: i,
+ element: s,
expectedType: o
} = t;
- e = n, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(s, e)
+ e = i, e += s ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
}
super(e), this.name = "ElementError"
}
@@ -125,6 +119,35 @@ class InitError extends GOVUKFrontendError {
super("string" == typeof t ? t : formatErrorMessage(t, "Root element (`$root`) already initialised")), this.name = "InitError"
}
}
+
+function normaliseDataset(Component, t) {
+ const e = {};
+ for (const [n, i] of Object.entries(Component.schema.properties)) n in t && (e[n] = normaliseString(t[n], i)), "object" === (null == i ? void 0 : i.type) && (e[n] = extractConfigByNamespace(Component, t, n));
+ return e
+}
+class Config {
+ static mergeConfigs(...t) {
+ const e = {};
+ for (const n of t)
+ for (const t of Object.keys(n)) {
+ const i = e[t],
+ s = n[t];
+ isObject(i) && isObject(s) ? e[t] = Config.mergeConfigs(i, s) : e[t] = s
+ }
+ return e
+ }
+ constructor(t, e, ...n) {
+ if (this.configObject = void 0, this.component = void 0, void 0 === t.defaults) throw new ConfigError("No defaults specified in component");
+ if (void 0 === t.schema) throw new ConfigError("No schema specified in component");
+ this.component = t;
+ const i = normaliseDataset(this.component, e);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...n, this.component.configOverride ? this.component.configOverride(i) : {}, i);
+ const s = this.configObject;
+ return new Proxy(this, {
+ get: (t, e, n) => Reflect.has(t, e) ? Reflect.get(t, e, n) : s[String(e)]
+ })
+ }
+}
class GOVUKFrontendComponent {
get $root() {
return this._$root
@@ -140,8 +163,8 @@ class GOVUKFrontendComponent {
expectedType: e.elementType.name
});
this._$root = t, e.checkSupport(), this.checkInitialised();
- const s = e.moduleName;
- this.$root.setAttribute(`data-${s}-init`, "")
+ const n = e.moduleName;
+ this.$root.setAttribute(`data-${n}-init`, "")
}
checkInitialised() {
const t = this.constructor,
@@ -157,31 +180,31 @@ class GOVUKFrontendComponent {
GOVUKFrontendComponent.elementType = HTMLElement;
class I18n {
constructor(t = {}, e = {}) {
- var s;
- this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (s = e.locale) ? s : document.documentElement.lang || "en"
+ 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");
- let s = this.translations[t];
- if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof s) {
- const n = s[this.getPluralSuffix(t, e.count)];
- n && (s = n)
+ let n = this.translations[t];
+ if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof n) {
+ const i = n[this.getPluralSuffix(t, e.count)];
+ i && (n = i)
}
- if ("string" == typeof s) {
- if (s.match(/%{(.\S+)}/)) {
+ 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(s, e)
+ return this.replacePlaceholders(n, e)
}
- return s
+ return n
}
return t
}
replacePlaceholders(t, e) {
- const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
- return t.replace(/%{(.\S+)}/g, (function(t, n) {
- if (Object.prototype.hasOwnProperty.call(e, n)) {
- const t = e[n];
- return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? s ? s.format(t) : `${t}` : 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
}
throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
}))
@@ -191,11 +214,11 @@ class I18n {
}
getPluralSuffix(t, e) {
if (e = Number(e), !isFinite(e)) return "other";
- const s = this.translations[t],
- n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
- if ("object" == typeof s) {
- if (n in s) return n;
- if ("other" in s) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+ const n = this.translations[t],
+ i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+ 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`)
}
@@ -207,8 +230,8 @@ class I18n {
getPluralRulesForLocale() {
const t = this.locale.split("-")[0];
for (const e in I18n.pluralRulesMap) {
- const s = I18n.pluralRulesMap[e];
- if (s.includes(this.locale) || s.includes(t)) return e
+ const n = I18n.pluralRulesMap[e];
+ if (n.includes(this.locale) || n.includes(t)) return e
}
}
}
@@ -230,8 +253,8 @@ I18n.pluralRulesMap = {
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,
- s = e % 10;
- return 1 === s && 11 !== e ? "one" : s >= 2 && s <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || e >= 11 && e <= 14 ? "many" : "other"
+ 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"
},
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",
@@ -239,13 +262,13 @@ I18n.pluralRulesMap = {
};
class Accordion extends GOVUKFrontendComponent {
constructor(t, e = {}) {
- super(t), 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.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, this.config = mergeConfigs(Accordion.defaults, e, normaliseDataset(Accordion, this.$root.dataset)), this.i18n = new I18n(this.config.i18n);
- const s = this.$root.querySelectorAll(`.${this.sectionClass}`);
- if (!s.length) throw new ElementError({
+ super(t), 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.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, this.config = new Config(Accordion, this.$root.dataset, e), this.i18n = new I18n(this.config.i18n);
+ const n = this.$root.querySelectorAll(`.${this.sectionClass}`);
+ if (!n.length) throw new ElementError({
component: Accordion,
identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
});
- this.$sections = s, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
+ this.$sections = n, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
}
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);
@@ -254,53 +277,53 @@ class Accordion extends GOVUKFrontendComponent {
}
initSectionHeaders() {
this.$sections.forEach(((t, e) => {
- const s = t.querySelector(`.${this.sectionHeaderClass}`);
- if (!s) throw new ElementError({
+ const n = t.querySelector(`.${this.sectionHeaderClass}`);
+ if (!n) throw new ElementError({
component: Accordion,
identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
});
- this.constructHeaderMarkup(s, e), this.setExpanded(this.isExpanded(t), t), s.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
+ this.constructHeaderMarkup(n, e), this.setExpanded(this.isExpanded(t), t), n.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
}))
}
constructHeaderMarkup(t, e) {
- const s = t.querySelector(`.${this.sectionButtonClass}`),
- n = t.querySelector(`.${this.sectionHeadingClass}`),
- i = t.querySelector(`.${this.sectionSummaryClass}`);
- if (!n) throw new ElementError({
+ const n = t.querySelector(`.${this.sectionButtonClass}`),
+ i = t.querySelector(`.${this.sectionHeadingClass}`),
+ s = t.querySelector(`.${this.sectionSummaryClass}`);
+ if (!i) throw new ElementError({
component: Accordion,
identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
});
- if (!s) throw new ElementError({
+ if (!n) throw new ElementError({
component: Accordion,
identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
});
const o = document.createElement("button");
o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$root.id}-content-${e+1}`);
- for (const d of Array.from(s.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
+ for (const d of Array.from(n.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
const r = document.createElement("span");
- r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
+ r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
const a = document.createElement("span");
- a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(s.childNodes).forEach((t => a.appendChild(t)));
+ a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(n.childNodes).forEach((t => a.appendChild(t)));
const c = document.createElement("span");
c.classList.add(this.sectionShowHideToggleClass), c.setAttribute("data-nosnippet", "");
const l = document.createElement("span");
l.classList.add(this.sectionShowHideToggleFocusClass), c.appendChild(l);
const h = document.createElement("span"),
u = document.createElement("span");
- if (u.classList.add(this.upChevronIconClass), l.appendChild(u), h.classList.add(this.sectionShowHideTextClass), l.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), i) {
+ if (u.classList.add(this.upChevronIconClass), l.appendChild(u), h.classList.add(this.sectionShowHideTextClass), l.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), s) {
const t = document.createElement("span"),
e = document.createElement("span");
e.classList.add(this.sectionSummaryFocusClass), t.appendChild(e);
- for (const s of Array.from(i.attributes)) t.setAttribute(s.name, s.value);
- Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
+ for (const n of Array.from(s.attributes)) t.setAttribute(n.name, n.value);
+ Array.from(s.childNodes).forEach((t => e.appendChild(t))), s.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
}
- o.appendChild(c), n.removeChild(s), n.appendChild(o)
+ o.appendChild(c), i.removeChild(n), i.appendChild(o)
}
onBeforeMatch(t) {
const e = t.target;
if (!(e instanceof Element)) return;
- const s = e.closest(`.${this.sectionClass}`);
- s && this.setExpanded(!0, s)
+ const n = e.closest(`.${this.sectionClass}`);
+ n && this.setExpanded(!0, n)
}
onSectionToggle(t) {
const e = !this.isExpanded(t);
@@ -313,24 +336,24 @@ class Accordion extends GOVUKFrontendComponent {
})), this.updateShowAllButton(t)
}
setExpanded(t, e) {
- const s = e.querySelector(`.${this.upChevronIconClass}`),
- n = e.querySelector(`.${this.sectionShowHideTextClass}`),
- i = e.querySelector(`.${this.sectionButtonClass}`),
+ const n = e.querySelector(`.${this.upChevronIconClass}`),
+ i = e.querySelector(`.${this.sectionShowHideTextClass}`),
+ s = e.querySelector(`.${this.sectionButtonClass}`),
o = e.querySelector(`.${this.sectionContentClass}`);
if (!o) throw new ElementError({
component: Accordion,
identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
});
- if (!s || !n || !i) return;
+ if (!n || !i || !s) return;
const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
- n.textContent = r, i.setAttribute("aria-expanded", `${t}`);
+ i.textContent = r, s.setAttribute("aria-expanded", `${t}`);
const a = [],
c = e.querySelector(`.${this.sectionHeadingTextClass}`);
c && a.push(`${c.textContent}`.trim());
const l = e.querySelector(`.${this.sectionSummaryClass}`);
l && a.push(`${l.textContent}`.trim());
const h = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
- a.push(h), i.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+ 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)), this.updateShowAllButton(this.areAllSectionsOpen())
}
isExpanded(t) {
return t.classList.contains(this.sectionExpandedClass)
@@ -347,18 +370,18 @@ class Accordion extends GOVUKFrontendComponent {
}
storeState(t, e) {
if (!this.config.rememberExpanded) return;
- const s = this.getIdentifier(t);
- if (s) try {
- window.sessionStorage.setItem(s, e.toString())
- } catch (n) {}
+ const n = this.getIdentifier(t);
+ if (n) try {
+ window.sessionStorage.setItem(n, e.toString())
+ } catch (i) {}
}
setInitialState(t) {
if (!this.config.rememberExpanded) return;
const e = this.getIdentifier(t);
if (e) try {
- const s = window.sessionStorage.getItem(e);
- null !== s && this.setExpanded("true" === s, t)
- } catch (s) {}
+ const n = window.sessionStorage.getItem(e);
+ null !== n && this.setExpanded("true" === n, t)
+ } catch (n) {}
}
getButtonPunctuationEl() {
const t = document.createElement("span");
@@ -387,7 +410,7 @@ Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
});
class Button extends GOVUKFrontendComponent {
constructor(t, e = {}) {
- super(t), this.config = void 0, this.debounceFormSubmitTimer = null, this.config = mergeConfigs(Button.defaults, e, normaliseDataset(Button, this.$root.dataset)), this.$root.addEventListener("keydown", (t => this.handleKeyDown(t))), this.$root.addEventListener("click", (t => this.debounce(t)))
+ super(t), this.config = void 0, this.debounceFormSubmitTimer = null, this.config = new Config(Button, this.$root.dataset, e), this.$root.addEventListener("keydown", (t => this.handleKeyDown(t))), this.$root.addEventListener("click", (t => this.debounce(t)))
}
handleKeyDown(t) {
const e = t.target;
@@ -401,8 +424,8 @@ class Button extends GOVUKFrontendComponent {
}
function closestAttributeValue(t, e) {
- const s = t.closest(`[${e}]`);
- return s ? s.getAttribute(e) : null
+ const n = t.closest(`[${e}]`);
+ return n ? n.getAttribute(e) : null
}
Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
preventDoubleClick: !1
@@ -415,54 +438,50 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
});
class CharacterCount extends GOVUKFrontendComponent {
constructor(t, e = {}) {
- var s, n;
+ var n, i;
super(t), 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;
- const i = this.$root.querySelector(".govuk-js-character-count");
- if (!(i instanceof HTMLTextAreaElement || i instanceof HTMLInputElement)) throw new ElementError({
+ const s = this.$root.querySelector(".govuk-js-character-count");
+ if (!(s instanceof HTMLTextAreaElement || s instanceof HTMLInputElement)) throw new ElementError({
component: CharacterCount,
- element: i,
+ element: s,
expectedType: "HTMLTextareaElement or HTMLInputElement",
identifier: "Form field (`.govuk-js-character-count`)"
});
- const o = normaliseDataset(CharacterCount, this.$root.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) {
- const s = [];
- for (const [n, i] of Object.entries(t)) {
- const t = [];
- if (Array.isArray(i)) {
+ this.config = new Config(CharacterCount, this.$root.dataset, e);
+ const o = function(t) {
+ const e = [],
+ n = t.component.schema;
+ for (const [i, s] of Object.entries(n)) {
+ const n = [];
+ if (Array.isArray(s)) {
for (const {
- required: s,
- errorMessage: n
+ required: e,
+ errorMessage: i
}
- of i) s.every((t => !!e[t])) || t.push(n);
- "anyOf" !== n || i.length - t.length >= 1 || s.push(...t)
+ of s) e.every((e => t.configObject[e])) || n.push(i);
+ "anyOf" !== i || s.length - n.length >= 1 || e.push(...n)
}
}
- return s
- }(CharacterCount.schema, this.config);
- if (a[0]) throw new ConfigError(formatErrorMessage(CharacterCount, a[0]));
+ return e
+ }(this.config);
+ if (o[0]) throw new ConfigError(formatErrorMessage(CharacterCount, o[0]));
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
- }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$textarea = i;
- const c = `${this.$textarea.id}-info`,
- l = document.getElementById(c);
- if (!l) throw new ElementError({
+ }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$textarea = s;
+ const r = `${this.$textarea.id}-info`,
+ a = document.getElementById(r);
+ if (!a) throw new ElementError({
component: CharacterCount,
- element: l,
- identifier: `Count message (\`id="${c}"\`)`
+ element: a,
+ identifier: `Count message (\`id="${r}"\`)`
});
- `${l.textContent}`.match(/^\s*$/) && (l.textContent = this.i18n.t("textareaDescription", {
+ `${a.textContent}`.match(/^\s*$/) && (a.textContent = this.i18n.t("textareaDescription", {
count: this.maxLength
- })), this.$textarea.insertAdjacentElement("afterend", l);
- const h = document.createElement("div");
- h.className = "govuk-character-count__sr-status govuk-visually-hidden", h.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = h, l.insertAdjacentElement("afterend", h);
- const u = document.createElement("div");
- u.className = l.className, u.classList.add("govuk-character-count__status"), u.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = u, l.insertAdjacentElement("afterend", u), l.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
+ })), this.$textarea.insertAdjacentElement("afterend", a);
+ const c = document.createElement("div");
+ c.className = "govuk-character-count__sr-status govuk-visually-hidden", c.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = c, a.insertAdjacentElement("afterend", c);
+ const l = document.createElement("div");
+ l.className = a.className, l.classList.add("govuk-character-count__status"), l.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = l, a.insertAdjacentElement("afterend", l), a.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
}
bindChangeEvents() {
this.$textarea.addEventListener("keyup", (() => this.handleKeyUp())), this.$textarea.addEventListener("focus", (() => this.handleFocus())), this.$textarea.addEventListener("blur", (() => this.handleBlur()))
@@ -505,8 +524,8 @@ class CharacterCount extends GOVUKFrontendComponent {
}
formatCountMessage(t, e) {
if (0 === t) return this.i18n.t(`${e}AtLimit`);
- const s = t < 0 ? "OverLimit" : "UnderLimit";
- return this.i18n.t(`${e}${s}`, {
+ const n = t < 0 ? "OverLimit" : "UnderLimit";
+ return this.i18n.t(`${e}${n}`, {
count: Math.abs(t)
})
}
@@ -541,7 +560,13 @@ CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = O
other: ""
}
}
-}), CharacterCount.schema = Object.freeze({
+}), CharacterCount.configOverride = t => {
+ let e = {};
+ return ("maxwords" in t || "maxlength" in t) && (e = {
+ maxlength: void 0,
+ maxwords: void 0
+ }), e
+}, CharacterCount.schema = Object.freeze({
properties: {
i18n: {
type: "object"
@@ -589,10 +614,10 @@ class Checkboxes extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(t) {
const e = t.getAttribute("aria-controls");
if (!e) return;
- const s = document.getElementById(e);
- if (null != s && s.classList.contains("govuk-checkboxes__conditional")) {
+ const n = document.getElementById(e);
+ if (null != n && n.classList.contains("govuk-checkboxes__conditional")) {
const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
+ t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
}
}
unCheckAllInputsExcept(t) {
@@ -625,25 +650,25 @@ class ErrorSummary extends GOVUKFrontendComponent {
if (!(t instanceof HTMLAnchorElement)) return !1;
const e = getFragmentFromUrl(t.href);
if (!e) return !1;
- const s = document.getElementById(e);
- if (!s) return !1;
- const n = this.getAssociatedLegendOrLabel(s);
- return !!n && (n.scrollIntoView(), s.focus({
+ const n = document.getElementById(e);
+ if (!n) return !1;
+ const i = this.getAssociatedLegendOrLabel(n);
+ return !!i && (i.scrollIntoView(), n.focus({
preventScroll: !0
}), !0)
}
getAssociatedLegendOrLabel(t) {
var e;
- const s = t.closest("fieldset");
- if (s) {
- const e = s.getElementsByTagName("legend");
+ const n = t.closest("fieldset");
+ if (n) {
+ const e = n.getElementsByTagName("legend");
if (e.length) {
- const s = e[0];
- if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return s;
- const n = s.getBoundingClientRect().top,
- i = t.getBoundingClientRect();
- if (i.height && window.innerHeight) {
- if (i.top + i.height - n < window.innerHeight / 2) return s
+ const n = e[0];
+ if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
+ const i = n.getBoundingClientRect().top,
+ s = t.getBoundingClientRect();
+ if (s.height && window.innerHeight) {
+ if (s.top + s.height - i < window.innerHeight / 2) return n
}
}
}
@@ -662,16 +687,16 @@ ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.
class ExitThisPage extends GOVUKFrontendComponent {
constructor(t, e = {}) {
super(t), 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;
- const s = this.$root.querySelector(".govuk-exit-this-page__button");
- if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
+ const n = this.$root.querySelector(".govuk-exit-this-page__button");
+ if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
component: ExitThisPage,
- element: s,
+ element: n,
expectedType: "HTMLAnchorElement",
identifier: "Button (`.govuk-exit-this-page__button`)"
});
- this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, this.$root.dataset)), this.i18n = new I18n(this.config.i18n), this.$button = s;
- const n = document.querySelector(".govuk-js-exit-this-page-skiplink");
- n instanceof HTMLAnchorElement && (this.$skiplinkButton = n), 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))
+ this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, this.$root.dataset)), this.i18n = new I18n(this.config.i18n), 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))
}
initUpdateSpan() {
this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$root.appendChild(this.$updateSpan)
@@ -737,18 +762,18 @@ class Header extends GOVUKFrontendComponent {
super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
const e = this.$root.querySelector(".govuk-js-header-toggle");
if (!e) return this;
- const s = e.getAttribute("aria-controls");
- if (!s) throw new ElementError({
+ const n = e.getAttribute("aria-controls");
+ if (!n) throw new ElementError({
component: Header,
identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
});
- const n = document.getElementById(s);
- if (!n) throw new ElementError({
+ const i = document.getElementById(n);
+ if (!i) throw new ElementError({
component: Header,
- element: n,
- identifier: `Navigation (\`<ul id="${s}">\`)`
+ element: i,
+ identifier: `Navigation (\`<ul id="${n}">\`)`
});
- this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("desktop");
@@ -783,27 +808,27 @@ NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.
class PasswordInput extends GOVUKFrontendComponent {
constructor(t, e = {}) {
super(t), this.config = void 0, this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0;
- const s = this.$root.querySelector(".govuk-js-password-input-input");
- if (!(s instanceof HTMLInputElement)) throw new ElementError({
+ const n = this.$root.querySelector(".govuk-js-password-input-input");
+ if (!(n instanceof HTMLInputElement)) throw new ElementError({
component: PasswordInput,
- element: s,
+ element: n,
expectedType: "HTMLInputElement",
identifier: "Form field (`.govuk-js-password-input-input`)"
});
- if ("password" !== s.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
- const n = this.$root.querySelector(".govuk-js-password-input-toggle");
- if (!(n instanceof HTMLButtonElement)) throw new ElementError({
+ if ("password" !== n.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
+ const i = this.$root.querySelector(".govuk-js-password-input-toggle");
+ if (!(i instanceof HTMLButtonElement)) throw new ElementError({
component: PasswordInput,
- element: n,
+ element: i,
expectedType: "HTMLButtonElement",
identifier: "Button (`.govuk-js-password-input-toggle`)"
});
- if ("button" !== n.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
- this.$input = s, this.$showHideButton = n, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, this.$root.dataset)), this.i18n = new I18n(this.config.i18n, {
+ if ("button" !== i.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+ this.$input = n, this.$showHideButton = i, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, this.$root.dataset)), this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
}), this.$showHideButton.removeAttribute("hidden");
- const i = document.createElement("div");
- i.className = "govuk-password-input__sr-status govuk-visually-hidden", i.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = i, this.$input.insertAdjacentElement("afterend", i), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
+ const s = document.createElement("div");
+ s.className = "govuk-password-input__sr-status govuk-visually-hidden", s.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = s, this.$input.insertAdjacentElement("afterend", s), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
t.persisted && "password" !== this.$input.type && this.hide()
})), this.hide()
}
@@ -820,9 +845,9 @@ class PasswordInput extends GOVUKFrontendComponent {
if (t === this.$input.type) return;
this.$input.setAttribute("type", t);
const e = "password" === t,
- s = e ? "show" : "hide",
- n = e ? "passwordHidden" : "passwordShown";
- this.$showHideButton.innerText = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
+ n = e ? "show" : "hide",
+ i = e ? "passwordHidden" : "passwordShown";
+ this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${i}Announcement`)
}
}
PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -866,21 +891,21 @@ class Radios extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(t) {
const e = t.getAttribute("aria-controls");
if (!e) return;
- const s = document.getElementById(e);
- if (null != s && s.classList.contains("govuk-radios__conditional")) {
+ const n = document.getElementById(e);
+ if (null != n && n.classList.contains("govuk-radios__conditional")) {
const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !e)
+ t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !e)
}
}
handleClick(t) {
const e = t.target;
if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
- const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
- n = e.form,
- i = e.name;
- s.forEach((t => {
- const e = t.form === n;
- t.name === i && e && this.syncConditionalRevealWithInputState(t)
+ 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)
}))
}
}
@@ -890,18 +915,18 @@ class ServiceNavigation extends GOVUKFrontendComponent {
super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
const e = this.$root.querySelector(".govuk-js-service-navigation-toggle");
if (!e) return this;
- const s = e.getAttribute("aria-controls");
- if (!s) throw new ElementError({
+ const n = e.getAttribute("aria-controls");
+ if (!n) throw new ElementError({
component: ServiceNavigation,
identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
});
- const n = document.getElementById(s);
- if (!n) throw new ElementError({
+ const i = document.getElementById(n);
+ if (!i) throw new ElementError({
component: ServiceNavigation,
- element: n,
- identifier: `Navigation (\`<ul id="${s}">\`)`
+ element: i,
+ identifier: `Navigation (\`<ul id="${n}">\`)`
});
- this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -923,17 +948,17 @@ class SkipLink extends GOVUKFrontendComponent {
constructor(t) {
var e;
super(t);
- const s = this.$root.hash,
- n = null != (e = this.$root.getAttribute("href")) ? e : "";
- let i;
+ const n = this.$root.hash,
+ i = null != (e = this.$root.getAttribute("href")) ? e : "";
+ let s;
try {
- i = new window.URL(this.$root.href)
+ s = new window.URL(this.$root.href)
} catch (a) {
- throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
+ throw new ElementError(`Skip link: Target link (\`href="${i}"\`) is invalid`)
}
- if (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
- const o = getFragmentFromUrl(s);
- if (!o) throw new ElementError(`Skip link: Target link (\`href="${n}"\`) has no hash fragment`);
+ if (s.origin !== window.location.origin || s.pathname !== window.location.pathname) return;
+ const o = getFragmentFromUrl(n);
+ if (!o) throw new ElementError(`Skip link: Target link (\`href="${i}"\`) has no hash fragment`);
const r = document.getElementById(o);
if (!r) throw new ElementError({
component: SkipLink,
@@ -960,17 +985,17 @@ class Tabs extends GOVUKFrontendComponent {
identifier: 'Links (`<a class="govuk-tabs__tab">`)'
});
this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
- const s = this.$root.querySelector(".govuk-tabs__list"),
- n = this.$root.querySelectorAll("li.govuk-tabs__list-item");
- if (!s) throw new ElementError({
+ const n = this.$root.querySelector(".govuk-tabs__list"),
+ i = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+ if (!n) throw new ElementError({
component: Tabs,
identifier: 'List (`<ul class="govuk-tabs__list">`)'
});
- if (!n.length) throw new ElementError({
+ if (!i.length) throw new ElementError({
component: Tabs,
identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
});
- this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
+ this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -1006,8 +1031,8 @@ class Tabs extends GOVUKFrontendComponent {
e = this.getTab(t);
if (!e) return;
if (this.changingHash) return void(this.changingHash = !1);
- const s = this.getCurrentTab();
- s && (this.hideTab(s), this.showTab(e), e.focus())
+ const n = this.getCurrentTab();
+ n && (this.hideTab(n), this.showTab(e), e.focus())
}
hideTab(t) {
this.unhighlightTab(t), this.hidePanel(t)
@@ -1022,8 +1047,8 @@ class Tabs extends GOVUKFrontendComponent {
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 s = this.getPanel(t);
- s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", t.id), s.classList.add(this.jsHiddenClass))
+ 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");
@@ -1032,14 +1057,14 @@ class Tabs extends GOVUKFrontendComponent {
}
onTabClick(t) {
const e = this.getCurrentTab(),
- s = t.currentTarget;
- e && s instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(s), this.createHistoryEntry(s))
+ 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 s = e.id;
- e.id = "", this.changingHash = !0, window.location.hash = s, e.id = s
+ const n = e.id;
+ e.id = "", this.changingHash = !0, window.location.hash = n, e.id = n
}
onTabKeydown(t) {
switch (t.key) {
@@ -1057,16 +1082,16 @@ class Tabs extends GOVUKFrontendComponent {
if (null == t || !t.parentElement) return;
const e = t.parentElement.nextElementSibling;
if (!e) return;
- const s = e.querySelector("a.govuk-tabs__tab");
- s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+ const n = e.querySelector("a.govuk-tabs__tab");
+ n && (this.hideTab(t), 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 s = e.querySelector("a.govuk-tabs__tab");
- s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+ const n = e.querySelector("a.govuk-tabs__tab");
+ n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
}
getPanel(t) {
const e = getFragmentFromUrl(t.href);
@@ -1096,7 +1121,7 @@ function initAll(t) {
if (t = void 0 !== t ? t : {}, !isSupported()) return void(t.onError ? t.onError(new SupportError, {
config: t
}) : console.log(new SupportError));
- const s = [
+ const n = [
[Accordion, t.accordion],
[Button, t.button],
[CharacterCount, t.characterCount],
@@ -1111,32 +1136,32 @@ function initAll(t) {
[SkipLink],
[Tabs]
],
- n = {
+ i = {
scope: null != (e = t.scope) ? e : document,
onError: t.onError
};
- s.forEach((([Component, t]) => {
- createAll(Component, t, n)
+ n.forEach((([Component, t]) => {
+ createAll(Component, t, i)
}))
}
function createAll(Component, t, e) {
- let s, n = document;
- var i;
- "object" == typeof e && (n = null != (i = e.scope) ? i : n, s = e.onError);
- "function" == typeof e && (s = e), e instanceof HTMLElement && (n = e);
- const o = n.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+ let n, i = document;
+ var s;
+ "object" == typeof e && (i = null != (s = e.scope) ? s : i, n = e.onError);
+ "function" == typeof e && (n = e), e instanceof HTMLElement && (i = e);
+ const o = i.querySelectorAll(`[data-module="${Component.moduleName}"]`);
return isSupported() ? Array.from(o).map((e => {
try {
return void 0 !== t ? new Component(e, t) : new Component(e)
- } catch (n) {
- return s ? s(n, {
+ } catch (i) {
+ return n ? n(i, {
element: e,
component: Component,
config: t
- }) : console.log(n), null
+ }) : console.log(i), null
}
- })).filter(Boolean) : (s ? s(new SupportError, {
+ })).filter(Boolean) : (n ? n(new SupportError, {
component: Component,
config: t
}) : console.log(new SupportError), [])
Action run for 629d6ee |
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 aad11d44a..1d03da0f3 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -133,8 +133,9 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
- function validateConfig(schema, config) {
+ function validateConfig(config) {
const validationErrors = [];
+ const schema = config.component.schema;
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
if (Array.isArray(conditions)) {
@@ -142,7 +143,7 @@
required,
errorMessage
} of conditions) {
- if (!required.every(key => !!config[key])) {
+ if (!required.every(key => config.configObject[key])) {
errors.push(errorMessage);
}
}
@@ -190,19 +191,6 @@
* @property {string} moduleName - Name of the component
*/
- function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
- }
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -256,6 +244,76 @@
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+ function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+ }
+
+ class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+ }
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -550,7 +608,7 @@
this.$showAllButton = null;
this.$showAllIcon = null;
this.$showAllText = null;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
+ this.config = new Config(Accordion, this.$root.dataset, config);
this.i18n = new I18n(this.config.i18n);
const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
@@ -861,7 +919,7 @@
super($root);
this.config = void 0;
this.debounceFormSubmitTimer = null;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
+ this.config = new Config(Button, this.$root.dataset, config);
this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
this.$root.addEventListener('click', event => this.debounce(event));
}
@@ -955,16 +1013,8 @@
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
- let configOverrides = {};
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
- configOverrides = {
- maxlength: undefined,
- maxwords: undefined
- };
- }
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
- const errors = validateConfig(CharacterCount.schema, this.config);
+ this.config = new Config(CharacterCount, this.$root.dataset, config);
+ const errors = validateConfig(this.config);
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
}
@@ -1179,6 +1229,22 @@
}
}
});
+ /**
+ * Override configuration
+ *
+ * @param {CharacterCountConfig} config - config to override
+ * @returns {CharacterCountConfig} - overidden config
+ */
+ CharacterCount.configOverride = config => {
+ let configOverrides = {};
+ if ('maxwords' in config || 'maxlength' in config) {
+ configOverrides = {
+ maxlength: undefined,
+ maxwords: undefined
+ };
+ }
+ return configOverrides;
+ };
CharacterCount.schema = Object.freeze({
properties: {
i18n: {
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index c72c0b957..940d63c49 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -127,8 +127,9 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
-function validateConfig(schema, config) {
+function validateConfig(config) {
const validationErrors = [];
+ const schema = config.component.schema;
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
if (Array.isArray(conditions)) {
@@ -136,7 +137,7 @@ function validateConfig(schema, config) {
required,
errorMessage
} of conditions) {
- if (!required.every(key => !!config[key])) {
+ if (!required.every(key => config.configObject[key])) {
errors.push(errorMessage);
}
}
@@ -184,19 +185,6 @@ function formatErrorMessage(Component, message) {
* @property {string} moduleName - Name of the component
*/
-function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
-}
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -250,6 +238,76 @@ class InitError extends GOVUKFrontendError {
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+}
+
+class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+}
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -544,7 +602,7 @@ class Accordion extends GOVUKFrontendComponent {
this.$showAllButton = null;
this.$showAllIcon = null;
this.$showAllText = null;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
+ this.config = new Config(Accordion, this.$root.dataset, config);
this.i18n = new I18n(this.config.i18n);
const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
@@ -855,7 +913,7 @@ class Button extends GOVUKFrontendComponent {
super($root);
this.config = void 0;
this.debounceFormSubmitTimer = null;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
+ this.config = new Config(Button, this.$root.dataset, config);
this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
this.$root.addEventListener('click', event => this.debounce(event));
}
@@ -949,16 +1007,8 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
- let configOverrides = {};
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
- configOverrides = {
- maxlength: undefined,
- maxwords: undefined
- };
- }
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
- const errors = validateConfig(CharacterCount.schema, this.config);
+ this.config = new Config(CharacterCount, this.$root.dataset, config);
+ const errors = validateConfig(this.config);
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
}
@@ -1173,6 +1223,22 @@ CharacterCount.defaults = Object.freeze({
}
}
});
+/**
+ * Override configuration
+ *
+ * @param {CharacterCountConfig} config - config to override
+ * @returns {CharacterCountConfig} - overidden config
+ */
+CharacterCount.configOverride = config => {
+ let configOverrides = {};
+ if ('maxwords' in config || 'maxlength' in config) {
+ configOverrides = {
+ maxlength: undefined,
+ maxwords: undefined
+ };
+ }
+ return configOverrides;
+};
CharacterCount.schema = Object.freeze({
properties: {
i18n: {
diff --git a/packages/govuk-frontend/dist/govuk/common/config.mjs b/packages/govuk-frontend/dist/govuk/common/config.mjs
new file mode 100644
index 000000000..28ce72a33
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/common/config.mjs
@@ -0,0 +1,63 @@
+import { ConfigError } from '../errors/index.mjs';
+import { isObject } from './index.mjs';
+import { normaliseDataset } from './normalise-dataset.mjs';
+
+class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+}
+
+export { Config as default };
+//# sourceMappingURL=config.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/common/index.mjs b/packages/govuk-frontend/dist/govuk/common/index.mjs
index 8c09a78a4..0112e6b66 100644
--- a/packages/govuk-frontend/dist/govuk/common/index.mjs
+++ b/packages/govuk-frontend/dist/govuk/common/index.mjs
@@ -98,8 +98,9 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
-function validateConfig(schema, config) {
+function validateConfig(config) {
const validationErrors = [];
+ const schema = config.component.schema;
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
if (Array.isArray(conditions)) {
@@ -107,7 +108,7 @@ function validateConfig(schema, config) {
required,
errorMessage
} of conditions) {
- if (!required.every(key => !!config[key])) {
+ if (!required.every(key => config.configObject[key])) {
errors.push(errorMessage);
}
}
@@ -155,5 +156,5 @@ function formatErrorMessage(Component, message) {
* @property {string} moduleName - Name of the component
*/
-export { extractConfigByNamespace, formatErrorMessage, getBreakpoint, getFragmentFromUrl, isInitialised, isSupported, mergeConfigs, setFocus, validateConfig };
+export { extractConfigByNamespace, formatErrorMessage, getBreakpoint, getFragmentFromUrl, isInitialised, isObject, isSupported, mergeConfigs, setFocus, validateConfig };
//# sourceMappingURL=index.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 925243064..034c9aff2 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -33,21 +33,6 @@
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
- function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -130,19 +115,6 @@
* @property {string} moduleName - Name of the component
*/
- function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
- }
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -161,6 +133,12 @@
this.name = 'SupportError';
}
}
+ class ConfigError extends GOVUKFrontendError {
+ constructor(...args) {
+ super(...args);
+ this.name = 'ConfigError';
+ }
+ }
class ElementError extends GOVUKFrontendError {
constructor(messageOrOptions) {
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -190,6 +168,76 @@
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+ function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+ }
+
+ class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+ }
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -484,7 +532,7 @@
this.$showAllButton = null;
this.$showAllIcon = null;
this.$showAllText = null;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
+ this.config = new Config(Accordion, this.$root.dataset, config);
this.i18n = new I18n(this.config.i18n);
const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
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 4b6542f05..4cba93d42 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -27,21 +27,6 @@ function normaliseString(value, property) {
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
-function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -124,19 +109,6 @@ function formatErrorMessage(Component, message) {
* @property {string} moduleName - Name of the component
*/
-function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
-}
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -155,6 +127,12 @@ class SupportError extends GOVUKFrontendError {
this.name = 'SupportError';
}
}
+class ConfigError extends GOVUKFrontendError {
+ constructor(...args) {
+ super(...args);
+ this.name = 'ConfigError';
+ }
+}
class ElementError extends GOVUKFrontendError {
constructor(messageOrOptions) {
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -184,6 +162,76 @@ class InitError extends GOVUKFrontendError {
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+}
+
+class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+}
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -478,7 +526,7 @@ class Accordion extends GOVUKFrontendComponent {
this.$showAllButton = null;
this.$showAllIcon = null;
this.$showAllText = null;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
+ this.config = new Config(Accordion, this.$root.dataset, config);
this.i18n = new I18n(this.config.i18n);
const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
index 528bdad14..ac1d3e220 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
@@ -1,5 +1,4 @@
-import { mergeConfigs } from '../../common/index.mjs';
-import { normaliseDataset } from '../../common/normalise-dataset.mjs';
+import Config from '../../common/config.mjs';
import { ElementError } from '../../errors/index.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
import { I18n } from '../../i18n.mjs';
@@ -50,7 +49,7 @@ class Accordion extends GOVUKFrontendComponent {
this.$showAllButton = null;
this.$showAllIcon = null;
this.$showAllText = null;
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
+ this.config = new Config(Accordion, this.$root.dataset, config);
this.i18n = new I18n(this.config.i18n);
const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
if (!$sections.length) {
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 e9ff03874..6792d8350 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
@@ -33,21 +33,6 @@
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
- function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -130,19 +115,6 @@
* @property {string} moduleName - Name of the component
*/
- function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
- }
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -161,6 +133,12 @@
this.name = 'SupportError';
}
}
+ class ConfigError extends GOVUKFrontendError {
+ constructor(...args) {
+ super(...args);
+ this.name = 'ConfigError';
+ }
+ }
class ElementError extends GOVUKFrontendError {
constructor(messageOrOptions) {
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -190,6 +168,76 @@
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+ function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+ }
+
+ class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+ }
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -261,7 +309,7 @@
super($root);
this.config = void 0;
this.debounceFormSubmitTimer = null;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
+ this.config = new Config(Button, this.$root.dataset, config);
this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
this.$root.addEventListener('click', event => this.debounce(event));
}
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 cd0977f2d..dd95fe406 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
@@ -27,21 +27,6 @@ function normaliseString(value, property) {
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
-function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -124,19 +109,6 @@ function formatErrorMessage(Component, message) {
* @property {string} moduleName - Name of the component
*/
-function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
-}
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -155,6 +127,12 @@ class SupportError extends GOVUKFrontendError {
this.name = 'SupportError';
}
}
+class ConfigError extends GOVUKFrontendError {
+ constructor(...args) {
+ super(...args);
+ this.name = 'ConfigError';
+ }
+}
class ElementError extends GOVUKFrontendError {
constructor(messageOrOptions) {
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -184,6 +162,76 @@ class InitError extends GOVUKFrontendError {
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+}
+
+class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+}
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -255,7 +303,7 @@ class Button extends GOVUKFrontendComponent {
super($root);
this.config = void 0;
this.debounceFormSubmitTimer = null;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
+ this.config = new Config(Button, this.$root.dataset, config);
this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
this.$root.addEventListener('click', event => this.debounce(event));
}
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
index b25a2f3a4..344311a69 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
@@ -1,5 +1,4 @@
-import { mergeConfigs } from '../../common/index.mjs';
-import { normaliseDataset } from '../../common/normalise-dataset.mjs';
+import Config from '../../common/config.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
@@ -18,7 +17,7 @@ class Button extends GOVUKFrontendComponent {
super($root);
this.config = void 0;
this.debounceFormSubmitTimer = null;
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
+ this.config = new Config(Button, this.$root.dataset, config);
this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
this.$root.addEventListener('click', event => this.debounce(event));
}
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 a092f98ef..6ae188ef5 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
@@ -38,21 +38,6 @@
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
- function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -98,8 +83,9 @@
}
return $scope.classList.contains('govuk-frontend-supported');
}
- function validateConfig(schema, config) {
+ function validateConfig(config) {
const validationErrors = [];
+ const schema = config.component.schema;
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
if (Array.isArray(conditions)) {
@@ -107,7 +93,7 @@
required,
errorMessage
} of conditions) {
- if (!required.every(key => !!config[key])) {
+ if (!required.every(key => config.configObject[key])) {
errors.push(errorMessage);
}
}
@@ -155,19 +141,6 @@
* @property {string} moduleName - Name of the component
*/
- function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
- }
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -221,6 +194,76 @@
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+ function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+ }
+
+ class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+ }
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -507,16 +550,8 @@
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
- let configOverrides = {};
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
- configOverrides = {
- maxlength: undefined,
- maxwords: undefined
- };
- }
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
- const errors = validateConfig(CharacterCount.schema, this.config);
+ this.config = new Config(CharacterCount, this.$root.dataset, config);
+ const errors = validateConfig(this.config);
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
}
@@ -731,6 +766,22 @@
}
}
});
+ /**
+ * Override configuration
+ *
+ * @param {CharacterCountConfig} config - config to override
+ * @returns {CharacterCountConfig} - overidden config
+ */
+ CharacterCount.configOverride = config => {
+ let configOverrides = {};
+ if ('maxwords' in config || 'maxlength' in config) {
+ configOverrides = {
+ maxlength: undefined,
+ maxwords: undefined
+ };
+ }
+ return configOverrides;
+ };
CharacterCount.schema = Object.freeze({
properties: {
i18n: {
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 d4e96618b..0c84e41ba 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
@@ -32,21 +32,6 @@ function normaliseString(value, property) {
* @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
*/
-function mergeConfigs(...configObjects) {
- const formattedConfigObject = {};
- for (const configObject of configObjects) {
- 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') {
@@ -92,8 +77,9 @@ function isSupported($scope = document.body) {
}
return $scope.classList.contains('govuk-frontend-supported');
}
-function validateConfig(schema, config) {
+function validateConfig(config) {
const validationErrors = [];
+ const schema = config.component.schema;
for (const [name, conditions] of Object.entries(schema)) {
const errors = [];
if (Array.isArray(conditions)) {
@@ -101,7 +87,7 @@ function validateConfig(schema, config) {
required,
errorMessage
} of conditions) {
- if (!required.every(key => !!config[key])) {
+ if (!required.every(key => config.configObject[key])) {
errors.push(errorMessage);
}
}
@@ -149,19 +135,6 @@ function formatErrorMessage(Component, message) {
* @property {string} moduleName - Name of the component
*/
-function normaliseDataset(Component, dataset) {
- const out = {};
- 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;
-}
-
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
@@ -215,6 +188,76 @@ class InitError extends GOVUKFrontendError {
* @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
*/
+function normaliseDataset(Component, dataset) {
+ const out = {};
+ 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;
+}
+
+class Config {
+ /**
+ * Merge configuration objects into a single config
+ *
+ * I think this makes sense to go in here rather then
+ * as utility function because it is used each time a
+ * configuration is created in the constructor of a component.
+ * So it would not be removed during tree-shaking.
+ *
+ * @param {...{[key:string]: unknown}} configObjects - configuration objects passed
+ * @returns {{[key:string]: unknown}} - merged configuration object
+ */
+ static mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = Config.mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+
+ /**
+ * @param {ComponentClass} component - Class of component using config
+ * @param {DOMStringMap} dataset - dataset of root component
+ * @param {...ConfigType} configObjects - Config objects to merge
+ */
+ constructor(component, dataset, ...configObjects) {
+ this.configObject = void 0;
+ this.component = void 0;
+ if (typeof component.defaults === 'undefined') {
+ throw new ConfigError('No defaults specified in component');
+ }
+ if (typeof component.schema === 'undefined') {
+ throw new ConfigError('No schema specified in component');
+ }
+ this.component = component;
+ const normalisedDataset = normaliseDataset(this.component, dataset);
+ this.configObject = Config.mergeConfigs(this.component.defaults, ...configObjects, this.component.configOverride ? this.component.configOverride(normalisedDataset) : {}, normalisedDataset);
+ const configObject = this.configObject;
+ return new Proxy(this, {
+ get(target, name, receiver) {
+ if (!Reflect.has(target, name)) {
+ return configObject[String(name)];
+ }
+ return Reflect.get(target, name, receiver);
+ }
+ });
+ }
+}
+
class GOVUKFrontendComponent {
/**
* Returns the root element of the component
@@ -501,16 +544,8 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
- let configOverrides = {};
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
- configOverrides = {
- maxlength: undefined,
- maxwords: undefined
- };
- }
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
- const errors = validateConfig(CharacterCount.schema, this.config);
+ this.config = new Config(CharacterCount, this.$root.dataset, config);
+ const errors = validateConfig(this.config);
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
}
@@ -725,6 +760,22 @@ CharacterCount.defaults = Object.freeze({
}
}
});
+/**
+ * Override configuration
+ *
+ * @param {CharacterCountConfig} config - config to override
+ * @returns {CharacterCountConfig} - overidden config
+ */
+CharacterCount.configOverride = config => {
+ let configOverrides = {};
+ if ('maxwords' in config || 'maxlength' in config) {
+ configOverrides = {
+ maxlength: undefined,
+ maxwords: undefined
+ };
+ }
+ return configOverrides;
+};
CharacterCount.schema = Object.freeze({
properties: {
i18n: {
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 84cd54d37..9d6fca02e 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,6 +1,6 @@
import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';
-import { mergeConfigs, validateConfig, formatErrorMessage } from '../../common/index.mjs';
-import { normaliseDataset } from '../../common/normalise-dataset.mjs';
+import Config from '../../common/config.mjs';
+import { validateConfig, formatErrorMessage } from '../../common/index.mjs';
import { ElementError, ConfigError } from '../../errors/index.mjs';
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
import { I18n } from '../../i18n.mjs';
@@ -43,16 +43,8 @@ class CharacterCount extends GOVUKFrontendComponent {
identifier: 'Form field (`.govuk-js-character-count`)'
});
}
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
- let configOverrides = {};
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
- configOverrides = {
- maxlength: undefined,
- maxwords: undefined
- };
- }
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
- const errors = validateConfig(CharacterCount.schema, this.config);
+ this.config = new Config(CharacterCount, this.$root.dataset, config);
+ const errors = validateConfig(this.config);
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
}
@@ -267,6 +259,22 @@ CharacterCount.defaults = Object.freeze({
}
}
});
+/**
+ * Override configuration
+ *
+ * @param {CharacterCountConfig} config - config to override
+ * @returns {CharacterCountConfig} - overidden config
+ */
+CharacterCount.configOverride = config => {
+ let configOverrides = {};
+ if ('maxwords' in config || 'maxlength' in config) {
+ configOverrides = {
+ maxlength: undefined,
+ maxwords: undefined
+ };
+ }
+ return configOverrides;
+};
CharacterCount.schema = Object.freeze({
properties: {
i18n: {
Action run for 629d6ee |
Config
class if defined by child class of GOVUKFrontendComponent
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.
Dropping thoughts ahead of tomorrow's discussion. Looks like this approach could be a stepping stone before encapsulating how configs are merged and pulled from the elements dataset (spiked in #5426).
Wondering if having a class creating a Proxy
is making typing more complex than we need it to be (with types like ConfigType<AccordionConfig> & AccordionConfig
). Maybe a function that returns an object would make things simpler on the type side, especially as all the class does is storing data (there's no method updating that data or whose behaviour change based on the data).
/** | ||
* Merge configuration objects into a single config | ||
* | ||
* I think this makes sense to go in here rather then | ||
* as utility function because it is used each time a | ||
* configuration is created in the constructor of a component. | ||
* So it would not be removed during tree-shaking. | ||
* | ||
* @param {...{[key:string]: unknown}} configObjects - configuration objects passed | ||
* @returns {{[key:string]: unknown}} - merged configuration object | ||
*/ | ||
static mergeConfigs(...configObjects) { |
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.
note: It's neat that it's been regrouped with the Config
class rather than be in common/index.mjs
. However, having it as a function in the module would allow its name to be mangled by minifiers (they leave methods alone as they may be called by outside code).
It'd likely need to be exported for unit tests, but we can document it as @internal
to keep its use to ourselves.
@@ -276,7 +276,7 @@ function isArray(option) { | |||
* @param {unknown} option - Option to check | |||
* @returns {boolean} Whether the option is an object | |||
*/ | |||
function isObject(option) { | |||
export function isObject(option) { |
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.
note Nice call leaving that one in index.mjs
.
if (typeof component.schema === 'undefined') { | ||
throw new ConfigError('No schema specified in component') | ||
} |
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.
note It doesn't seem the class does anything of the schema
beyond storing it. Thinking we can leave that out of the class for now (for simplicity), at least until we look at building validation inside the class).
/** | ||
* @typedef {{new (...args: any[]): any, moduleName: string, schema?: {[key:string]: unknown}, defaults?: {[key:string]: unknown} }} ComponentClass | ||
*/ |
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.
question Do you think we need to review the types around what makes a component class? Thinking we have that in a couple of places now (around createAll
, around the GOVUKFrontendComponent
class) and we may want to organise this a little bit into proper concepts: a constructor + having a moduleName
+ having configuration static properties (defaults
at least).
* @returns {string[]} List of validation errors | ||
*/ | ||
export function validateConfig(schema, config) { | ||
export function validateConfig(config) { |
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.
issue I'd be keen to leave the validation completely outside the work on Config
for now, until we figure whether we want to validate the configuration as part of our public API. That would avoid the Config
object needing to store the schema
.
// Check errors for each schema | ||
for (const [name, conditions] of Object.entries(schema)) { | ||
const errors = [] | ||
|
||
// Check errors for each schema condition | ||
if (Array.isArray(conditions)) { | ||
for (const { required, errorMessage } of conditions) { | ||
if (!required.every((key) => !!config[key])) { | ||
if (!required.every((key) => config.configObject[key])) { |
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.
question Given the Config
is a Proxy
, what prevents us from accessing the key
via config
directly? Is it TypeScript?
/** | ||
* @private | ||
* @type {AccordionConfig} | ||
* @type {Config<AccordionConfig> & AccordionConfig} |
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.
question I'm not understanding the combination of types on this one, sorry. What's requiring us to combine those?
static configOverride = (config) => { | ||
/** @type {CharacterCountConfig} */ | ||
let configOverrides = {} | ||
if ('maxwords' in config || 'maxlength' in config) { | ||
configOverrides = { | ||
maxlength: undefined, | ||
maxwords: undefined | ||
} | ||
} |
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.
note I like the way you've solved the issue here. 🙌🏻
Thinking we should keep that feature @internal
for now, though, as it's mostly due to the current set of options in this component. Then if people come asking for such feature for their components, we can look at offering it as part of the public API (and consider naming...). What are your thoughts?
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.
suggestion Static methods are natively supported in browsers we transpile to, while static fields will require extra code.
static configOverride = (config) => { | |
/** @type {CharacterCountConfig} */ | |
let configOverrides = {} | |
if ('maxwords' in config || 'maxlength' in config) { | |
configOverrides = { | |
maxlength: undefined, | |
maxwords: undefined | |
} | |
} | |
static configOverride(config) { | |
/** @type {CharacterCountConfig} */ | |
let configOverrides = {} | |
if ('maxwords' in config || 'maxlength' in config) { | |
configOverrides = { | |
maxlength: undefined, | |
maxwords: undefined | |
} | |
} |
@@ -116,7 +126,8 @@ class Config { | |||
**/ | |||
|
|||
/** | |||
* @typedef {{new (...args: any[]): any, moduleName: string, schema: {[key:string]: unknown}, defaults: {[key:string]: unknown} }} CompatibleClass | |||
* @template {{[key:string]: unknown}} [ConfigType=ObjectNested] | |||
* @typedef {{new (...args: any[]): any, moduleName: string, schema: import('./index.mjs').Schema, defaults: ConfigType, configOverride?: (config: ConfigType) => ConfigType }} CompatibleClass |
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.
nitpick Thinking CompatibleClass
is a little generic here (it's also the case for the one used with createAll
), can we maybe name it ConfigurableComponent
or similar?
* | ||
* @template {{[key:string]: unknown}} [ConfigType=ObjectNested] | ||
*/ | ||
class Config { |
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.
nitpick Playing around in VSCode, the name config
leads to all .config.js
file appearing when looking for the file with Ctrl+P
. Configuration
would avoid this.
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.
issue We prefer named exports, which limit the risks of name being changed when import
ed, making it easier to debug.
class Config { | |
export class Config { |
In this spike, the
Config
is defined in the child component. As well as accepting aComponent
class (with a defined schema and defined defaults) and set of config objects, it also requires the dataset of theroot
of the component (fornormaliseDataset
and override functionality.