UNPKG

digitalmarketplace-frontend

Version:

Digital Marketplace Frontend contains assets and components used in Digital Marketplace projects

1 lines 246 kB
{"version":3,"file":"all.bundle.mjs","sources":["../../src/digitalmarketplace/common/digitalmarketplace-frontend-version.ts","../../src/digitalmarketplace/scripts/analytics/tag-manager.ts","../../../../node_modules/js-cookie/dist/js.cookie.mjs","../../src/digitalmarketplace/scripts/analytics/data-layer.types.ts","../../src/digitalmarketplace/scripts/analytics/data-layer.ts","../../src/digitalmarketplace/scripts/cookies/manage.ts","../../src/digitalmarketplace/scripts/analytics/init-google-analytics.ts","../../../../node_modules/govuk-frontend/dist/govuk/common/index.mjs","../../../../node_modules/govuk-frontend/dist/govuk/errors/index.mjs","../../../../node_modules/govuk-frontend/dist/govuk/component.mjs","../../../../node_modules/govuk-frontend/dist/govuk/common/configuration.mjs","../../../../node_modules/govuk-frontend/dist/govuk/i18n.mjs","../../../../node_modules/govuk-frontend/dist/govuk/common/closest-attribute-value.mjs","../../../../node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs","../../src/digitalmarketplace/components/compliance-communication-attachments/compliance-communication-attachments.ts","../../src/digitalmarketplace/components/cookie-banner/cookie-banner.ts","../../src/digitalmarketplace/scripts/cookies/settings.ts","../../src/digitalmarketplace/components/list-input/list-input.ts","../../src/digitalmarketplace/components/question/question.ts","../../src/digitalmarketplace/components/question-checkbox-tree/question-checkbox-tree.ts","../../src/digitalmarketplace/components/question-list/question-list.ts","../../src/digitalmarketplace/components/question-list-multiquestion-client-side/question-list-multiquestion-client-side.ts","../../src/digitalmarketplace/components/option-select/option-select.ts","../../src/digitalmarketplace/components/search-box/search-box.ts","../../../../node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js","../../src/digitalmarketplace/components/select-country-from-form/select-country-from-form.ts","../../src/digitalmarketplace/init.ts"],"sourcesContent":["/*\n * This variable is automatically overwritten during builds and releases.\n * It doesn't need to be updated manually.\n */\n\n/**\n * Digital Marketplace Frontend release version\n *\n * {@link https://github.com/Crown-Commercial-Service/ccs-digitalmarketplace-govuk-frontend/releases}\n */\nexport const version = 'development'\n","const GOOGLE_TAG_MANAGER_ID = 'GTM-WCGFX3GW'\n\nconst googleTagManagerInit = (w: Window, d: Document, s: 'script', l: 'dataLayer', i: string) => {\n w[l] = w[l] || []\n w[l].push({\n 'gtm.start': new Date().getTime(),\n event: 'gtm.js'\n })\n const f = d.getElementsByTagName(s)[0]\n const j = d.createElement(s)\n const dl = l != 'dataLayer' ? '&l=' + l : ''\n j.async = true\n j.src ='https://www.googletagmanager.com/gtm.js?id=' + i + dl;\n (f.parentNode as ParentNode).insertBefore(j, f)\n}\n\nconst loadGoogleTagManager = () => googleTagManagerInit(window, document, 'script', 'dataLayer', GOOGLE_TAG_MANAGER_ID)\n\nexport { loadGoogleTagManager }\n","/*! js-cookie v3.0.5 | MIT */\n/* eslint-disable no-var */\nfunction assign (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n target[key] = source[key];\n }\n }\n return target\n}\n/* eslint-enable no-var */\n\n/* eslint-disable no-var */\nvar defaultConverter = {\n read: function (value) {\n if (value[0] === '\"') {\n value = value.slice(1, -1);\n }\n return value.replace(/(%[\\dA-F]{2})+/gi, decodeURIComponent)\n },\n write: function (value) {\n return encodeURIComponent(value).replace(\n /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,\n decodeURIComponent\n )\n }\n};\n/* eslint-enable no-var */\n\n/* eslint-disable no-var */\n\nfunction init (converter, defaultAttributes) {\n function set (name, value, attributes) {\n if (typeof document === 'undefined') {\n return\n }\n\n attributes = assign({}, defaultAttributes, attributes);\n\n if (typeof attributes.expires === 'number') {\n attributes.expires = new Date(Date.now() + attributes.expires * 864e5);\n }\n if (attributes.expires) {\n attributes.expires = attributes.expires.toUTCString();\n }\n\n name = encodeURIComponent(name)\n .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)\n .replace(/[()]/g, escape);\n\n var stringifiedAttributes = '';\n for (var attributeName in attributes) {\n if (!attributes[attributeName]) {\n continue\n }\n\n stringifiedAttributes += '; ' + attributeName;\n\n if (attributes[attributeName] === true) {\n continue\n }\n\n // Considers RFC 6265 section 5.2:\n // ...\n // 3. If the remaining unparsed-attributes contains a %x3B (\";\")\n // character:\n // Consume the characters of the unparsed-attributes up to,\n // not including, the first %x3B (\";\") character.\n // ...\n stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];\n }\n\n return (document.cookie =\n name + '=' + converter.write(value, name) + stringifiedAttributes)\n }\n\n function get (name) {\n if (typeof document === 'undefined' || (arguments.length && !name)) {\n return\n }\n\n // To prevent the for loop in the first place assign an empty array\n // in case there are no cookies at all.\n var cookies = document.cookie ? document.cookie.split('; ') : [];\n var jar = {};\n for (var i = 0; i < cookies.length; i++) {\n var parts = cookies[i].split('=');\n var value = parts.slice(1).join('=');\n\n try {\n var found = decodeURIComponent(parts[0]);\n jar[found] = converter.read(value, found);\n\n if (name === found) {\n break\n }\n } catch (e) {}\n }\n\n return name ? jar[name] : jar\n }\n\n return Object.create(\n {\n set,\n get,\n remove: function (name, attributes) {\n set(\n name,\n '',\n assign({}, attributes, {\n expires: -1\n })\n );\n },\n withAttributes: function (attributes) {\n return init(this.converter, assign({}, this.attributes, attributes))\n },\n withConverter: function (converter) {\n return init(assign({}, this.converter, converter), this.attributes)\n }\n },\n {\n attributes: { value: Object.freeze(defaultAttributes) },\n converter: { value: Object.freeze(converter) }\n }\n )\n}\n\nvar api = init(defaultConverter, { path: '/' });\n/* eslint-enable no-var */\n\nexport { api as default };\n","\nenum GrantType {\n GRANTED = 'granted',\n NOT_GRANTED = 'not granted'\n}\n\nexport { GrantType }","import { CookiePreferences } from '../cookies/manage.types'\nimport { GrantType } from './data-layer.types'\n\nconst getGrantedText = (state: boolean) => state ? GrantType.GRANTED : GrantType.NOT_GRANTED\n\nconst updateDataLayer = (cookiePreferences: CookiePreferences) => {\n window.dataLayer.push({\n event: 'gtm_consent_update',\n usage_consent: getGrantedText(cookiePreferences.usage),\n glassbox_consent: getGrantedText(cookiePreferences.glassbox),\n marketing_consent: GrantType.NOT_GRANTED\n })\n}\n\nexport { updateDataLayer }","import Cookies from 'js-cookie'\nimport { CookiePreferences, CookieUpdateOption } from './manage.types'\nimport { updateDataLayer } from '../analytics/data-layer'\n\nconst cookieUpdateOptions: CookieUpdateOption[] = [\n {\n cookieName: 'usage',\n cookiePrefixes: ['_ga', '_gi']\n },\n {\n cookieName: 'glassbox',\n cookiePrefixes: ['_cls']\n }\n]\n\nconst DOMAINS = [\n '.marketplace.team',\n '.digitalmarketplace.service.gov.uk',\n 'www.applytosupply.digitalmarketplace.service.gov.uk'\n]\n\n// When not in production we want to delete cookies in localhost too\nconst getDomains = () => {\n if (window.location.hostname === 'localhost') {\n return DOMAINS.concat(['localhost'])\n }\n\n return DOMAINS\n}\n\nconst getCookiePreferences = (): CookiePreferences => {\n const defaultCookieSettings = '{\"usage\":false,\"glassbox\":false}'\n\n return JSON.parse(Cookies.get('cookie_preferences_dmp') ?? defaultCookieSettings)\n}\n\nconst setCookiePreferences = (usage: boolean) => {\n const cookiePreferences: CookiePreferences = getCookiePreferences()\n\n cookiePreferences.usage = usage\n cookiePreferences.settings_viewed = true\n\n Cookies.set('cookie_preferences_dmp', JSON.stringify(cookiePreferences), { expires: 365 })\n\n updateDataLayer(cookiePreferences)\n removeUnwantedCookies()\n}\n\nconst removeUnwantedCookies = (): void => {\n const cookieList: string[] = Object.keys(Cookies.get())\n const cookiesToRemove: string[] = ['digitalmarketplace_cookie_settings_viewed', 'digitalmarketplace_google_analytics_enabled', 'digitalmarketplace_cookie_options_v1', 'dm_cookies_policy']\n const cookiePreferences: CookiePreferences = getCookiePreferences()\n const cookiePrefixes: string[] = []\n\n cookieUpdateOptions.forEach((cookieUpdateOption) => {\n if (!cookiePreferences[cookieUpdateOption.cookieName]) cookiePrefixes.push(...cookieUpdateOption.cookiePrefixes)\n })\n\n for (let i = 0; i < cookieList.length; i++) {\n const cookieName = cookieList[i]\n\n if (cookiePrefixes.some((cookiePrefix) => cookieName.startsWith(cookiePrefix))) cookiesToRemove.push(cookieName)\n }\n\n getDomains().forEach((domain) => {\n cookiesToRemove.forEach((cookieName) => { Cookies.remove(cookieName, { path: '/', domain: domain }) })\n })\n}\n\nexport {\n getCookiePreferences,\n setCookiePreferences,\n removeUnwantedCookies\n}","import { loadGoogleTagManager } from './tag-manager'\nimport { removeUnwantedCookies } from '../cookies/manage'\n\nconst initGoogleAnalytics = () => {\n loadGoogleTagManager()\n\n removeUnwantedCookies()\n}\n\nexport { initGoogleAnalytics }\n","function getFragmentFromUrl(url) {\n if (!url.includes('#')) {\n return undefined;\n }\n return url.split('#').pop();\n}\nfunction getBreakpoint(name) {\n const property = `--govuk-frontend-breakpoint-${name}`;\n const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);\n return {\n property,\n value: value || undefined\n };\n}\nfunction setFocus($element, options = {}) {\n var _options$onBeforeFocu;\n const isFocusable = $element.getAttribute('tabindex');\n if (!isFocusable) {\n $element.setAttribute('tabindex', '-1');\n }\n function onFocus() {\n $element.addEventListener('blur', onBlur, {\n once: true\n });\n }\n function onBlur() {\n var _options$onBlur;\n (_options$onBlur = options.onBlur) == null || _options$onBlur.call($element);\n if (!isFocusable) {\n $element.removeAttribute('tabindex');\n }\n }\n $element.addEventListener('focus', onFocus, {\n once: true\n });\n (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);\n $element.focus();\n}\nfunction isInitialised($root, moduleName) {\n return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);\n}\n\n/**\n * Checks if GOV.UK Frontend is supported on this page\n *\n * Some browsers will load and run our JavaScript but GOV.UK Frontend\n * won't be supported.\n *\n * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support\n * @returns {boolean} Whether GOV.UK Frontend is supported on this page\n */\nfunction isSupported($scope = document.body) {\n if (!$scope) {\n return false;\n }\n return $scope.classList.contains('govuk-frontend-supported');\n}\nfunction isArray(option) {\n return Array.isArray(option);\n}\nfunction isObject(option) {\n return !!option && typeof option === 'object' && !isArray(option);\n}\nfunction formatErrorMessage(Component, message) {\n return `${Component.moduleName}: ${message}`;\n}\n/**\n * @typedef ComponentWithModuleName\n * @property {string} moduleName - Name of the component\n */\n/**\n * @import { ObjectNested } from './configuration.mjs'\n */\n\nexport { formatErrorMessage, getBreakpoint, getFragmentFromUrl, isInitialised, isObject, isSupported, setFocus };\n//# sourceMappingURL=index.mjs.map\n","import { formatErrorMessage } from '../common/index.mjs';\n\nclass GOVUKFrontendError extends Error {\n constructor(...args) {\n super(...args);\n this.name = 'GOVUKFrontendError';\n }\n}\nclass SupportError extends GOVUKFrontendError {\n /**\n * Checks if GOV.UK Frontend is supported on this page\n *\n * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support\n */\n constructor($scope = document.body) {\n const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class=\"govuk-frontend-supported\">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';\n super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type=\"module\">`');\n this.name = 'SupportError';\n }\n}\nclass ConfigError extends GOVUKFrontendError {\n constructor(...args) {\n super(...args);\n this.name = 'ConfigError';\n }\n}\nclass ElementError extends GOVUKFrontendError {\n constructor(messageOrOptions) {\n let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';\n if (typeof messageOrOptions === 'object') {\n const {\n component,\n identifier,\n element,\n expectedType\n } = messageOrOptions;\n message = identifier;\n message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';\n message = formatErrorMessage(component, message);\n }\n super(message);\n this.name = 'ElementError';\n }\n}\nclass InitError extends GOVUKFrontendError {\n constructor(componentOrMessage) {\n const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\\`$root\\`) already initialised`);\n super(message);\n this.name = 'InitError';\n }\n}\n/**\n * @import { ComponentWithModuleName } from '../common/index.mjs'\n */\n\nexport { ConfigError, ElementError, GOVUKFrontendError, InitError, SupportError };\n//# sourceMappingURL=index.mjs.map\n","import { isInitialised, isSupported } from './common/index.mjs';\nimport { InitError, ElementError, SupportError } from './errors/index.mjs';\n\nclass Component {\n /**\n * Returns the root element of the component\n *\n * @protected\n * @returns {RootElementType} - the root element of component\n */\n get $root() {\n return this._$root;\n }\n constructor($root) {\n this._$root = void 0;\n const childConstructor = this.constructor;\n if (typeof childConstructor.moduleName !== 'string') {\n throw new InitError(`\\`moduleName\\` not defined in component`);\n }\n if (!($root instanceof childConstructor.elementType)) {\n throw new ElementError({\n element: $root,\n component: childConstructor,\n identifier: 'Root element (`$root`)',\n expectedType: childConstructor.elementType.name\n });\n } else {\n this._$root = $root;\n }\n childConstructor.checkSupport();\n this.checkInitialised();\n const moduleName = childConstructor.moduleName;\n this.$root.setAttribute(`data-${moduleName}-init`, '');\n }\n checkInitialised() {\n const constructor = this.constructor;\n const moduleName = constructor.moduleName;\n if (moduleName && isInitialised(this.$root, moduleName)) {\n throw new InitError(constructor);\n }\n }\n static checkSupport() {\n if (!isSupported()) {\n throw new SupportError();\n }\n }\n}\n\n/**\n * @typedef ChildClass\n * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component\n */\n\n/**\n * @typedef {typeof Component & ChildClass} ChildClassConstructor\n */\nComponent.elementType = HTMLElement;\n\nexport { Component };\n//# sourceMappingURL=component.mjs.map\n","import { Component } from '../component.mjs';\nimport { ConfigError } from '../errors/index.mjs';\nimport { isObject, formatErrorMessage } from './index.mjs';\n\nconst configOverride = Symbol.for('configOverride');\nclass ConfigurableComponent extends Component {\n [configOverride](param) {\n return {};\n }\n\n /**\n * Returns the root element of the component\n *\n * @protected\n * @returns {ConfigurationType} - the root element of component\n */\n get config() {\n return this._config;\n }\n constructor($root, config) {\n super($root);\n this._config = void 0;\n const childConstructor = this.constructor;\n if (!isObject(childConstructor.defaults)) {\n throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));\n }\n const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);\n this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);\n }\n}\nfunction normaliseString(value, property) {\n const trimmedValue = value ? value.trim() : '';\n let output;\n let outputType = property == null ? void 0 : property.type;\n if (!outputType) {\n if (['true', 'false'].includes(trimmedValue)) {\n outputType = 'boolean';\n }\n if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {\n outputType = 'number';\n }\n }\n switch (outputType) {\n case 'boolean':\n output = trimmedValue === 'true';\n break;\n case 'number':\n output = Number(trimmedValue);\n break;\n default:\n output = value;\n }\n return output;\n}\nfunction normaliseDataset(Component, dataset) {\n if (!isObject(Component.schema)) {\n throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));\n }\n const out = {};\n const entries = Object.entries(Component.schema.properties);\n for (const entry of entries) {\n const [namespace, property] = entry;\n const field = namespace.toString();\n if (field in dataset) {\n out[field] = normaliseString(dataset[field], property);\n }\n if ((property == null ? void 0 : property.type) === 'object') {\n out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);\n }\n }\n return out;\n}\nfunction mergeConfigs(...configObjects) {\n const formattedConfigObject = {};\n for (const configObject of configObjects) {\n for (const key of Object.keys(configObject)) {\n const option = formattedConfigObject[key];\n const override = configObject[key];\n if (isObject(option) && isObject(override)) {\n formattedConfigObject[key] = mergeConfigs(option, override);\n } else {\n formattedConfigObject[key] = override;\n }\n }\n }\n return formattedConfigObject;\n}\nfunction validateConfig(schema, config) {\n const validationErrors = [];\n for (const [name, conditions] of Object.entries(schema)) {\n const errors = [];\n if (Array.isArray(conditions)) {\n for (const {\n required,\n errorMessage\n } of conditions) {\n if (!required.every(key => !!config[key])) {\n errors.push(errorMessage);\n }\n }\n if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {\n validationErrors.push(...errors);\n }\n }\n }\n return validationErrors;\n}\nfunction extractConfigByNamespace(schema, dataset, namespace) {\n const property = schema.properties[namespace];\n if ((property == null ? void 0 : property.type) !== 'object') {\n return;\n }\n const newObject = {\n [namespace]: {}\n };\n for (const [key, value] of Object.entries(dataset)) {\n let current = newObject;\n const keyParts = key.split('.');\n for (const [index, name] of keyParts.entries()) {\n if (isObject(current)) {\n if (index < keyParts.length - 1) {\n if (!isObject(current[name])) {\n current[name] = {};\n }\n current = current[name];\n } else if (key !== namespace) {\n current[name] = normaliseString(value);\n }\n }\n }\n }\n return newObject[namespace];\n}\n/**\n * Schema for component config\n *\n * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType\n * @typedef {object} Schema\n * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties\n * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions\n */\n/**\n * Schema property for component config\n *\n * @typedef {object} SchemaProperty\n * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type\n */\n/**\n * Schema condition for component config\n *\n * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType\n * @typedef {object} SchemaCondition\n * @property {(keyof ConfigurationType)[]} required - List of required config fields\n * @property {string} errorMessage - Error message when required config fields not provided\n */\n/**\n * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]\n * @typedef ChildClass\n * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component\n * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration\n * @property {ConfigurationType} [defaults] - The default values of the configuration of the component\n */\n/**\n * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]\n * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>\n */\n\nexport { ConfigurableComponent, configOverride, extractConfigByNamespace, mergeConfigs, normaliseDataset, normaliseString, validateConfig };\n//# sourceMappingURL=configuration.mjs.map\n","class I18n {\n constructor(translations = {}, config = {}) {\n var _config$locale;\n this.translations = void 0;\n this.locale = void 0;\n this.translations = translations;\n this.locale = (_config$locale = config.locale) != null ? _config$locale : document.documentElement.lang || 'en';\n }\n t(lookupKey, options) {\n if (!lookupKey) {\n throw new Error('i18n: lookup key missing');\n }\n let translation = this.translations[lookupKey];\n if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {\n const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];\n if (translationPluralForm) {\n translation = translationPluralForm;\n }\n }\n if (typeof translation === 'string') {\n if (translation.match(/%{(.\\S+)}/)) {\n if (!options) {\n throw new Error('i18n: cannot replace placeholders in string if no option data provided');\n }\n return this.replacePlaceholders(translation, options);\n }\n return translation;\n }\n return lookupKey;\n }\n replacePlaceholders(translationString, options) {\n const formatter = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : undefined;\n return translationString.replace(/%{(.\\S+)}/g, function (placeholderWithBraces, placeholderKey) {\n if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {\n const placeholderValue = options[placeholderKey];\n if (placeholderValue === false || typeof placeholderValue !== 'number' && typeof placeholderValue !== 'string') {\n return '';\n }\n if (typeof placeholderValue === 'number') {\n return formatter ? formatter.format(placeholderValue) : `${placeholderValue}`;\n }\n return placeholderValue;\n }\n throw new Error(`i18n: no data found to replace ${placeholderWithBraces} placeholder in string`);\n });\n }\n hasIntlPluralRulesSupport() {\n return Boolean('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length);\n }\n getPluralSuffix(lookupKey, count) {\n count = Number(count);\n if (!isFinite(count)) {\n return 'other';\n }\n const translation = this.translations[lookupKey];\n const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);\n if (typeof translation === 'object') {\n if (preferredForm in translation) {\n return preferredForm;\n } else if ('other' in translation) {\n console.warn(`i18n: Missing plural form \".${preferredForm}\" for \"${this.locale}\" locale. Falling back to \".other\".`);\n return 'other';\n }\n }\n throw new Error(`i18n: Plural form \".other\" is required for \"${this.locale}\" locale`);\n }\n selectPluralFormUsingFallbackRules(count) {\n count = Math.abs(Math.floor(count));\n const ruleset = this.getPluralRulesForLocale();\n if (ruleset) {\n return I18n.pluralRules[ruleset](count);\n }\n return 'other';\n }\n getPluralRulesForLocale() {\n const localeShort = this.locale.split('-')[0];\n for (const pluralRule in I18n.pluralRulesMap) {\n const languages = I18n.pluralRulesMap[pluralRule];\n if (languages.includes(this.locale) || languages.includes(localeShort)) {\n return pluralRule;\n }\n }\n }\n}\nI18n.pluralRulesMap = {\n arabic: ['ar'],\n chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],\n french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],\n german: ['af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka', 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'],\n irish: ['ga'],\n russian: ['ru', 'uk'],\n scottish: ['gd'],\n spanish: ['pt-PT', 'it', 'es'],\n welsh: ['cy']\n};\nI18n.pluralRules = {\n arabic(n) {\n if (n === 0) {\n return 'zero';\n }\n if (n === 1) {\n return 'one';\n }\n if (n === 2) {\n return 'two';\n }\n if (n % 100 >= 3 && n % 100 <= 10) {\n return 'few';\n }\n if (n % 100 >= 11 && n % 100 <= 99) {\n return 'many';\n }\n return 'other';\n },\n chinese() {\n return 'other';\n },\n french(n) {\n return n === 0 || n === 1 ? 'one' : 'other';\n },\n german(n) {\n return n === 1 ? 'one' : 'other';\n },\n irish(n) {\n if (n === 1) {\n return 'one';\n }\n if (n === 2) {\n return 'two';\n }\n if (n >= 3 && n <= 6) {\n return 'few';\n }\n if (n >= 7 && n <= 10) {\n return 'many';\n }\n return 'other';\n },\n russian(n) {\n const lastTwo = n % 100;\n const last = lastTwo % 10;\n if (last === 1 && lastTwo !== 11) {\n return 'one';\n }\n if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {\n return 'few';\n }\n if (last === 0 || last >= 5 && last <= 9 || lastTwo >= 11 && lastTwo <= 14) {\n return 'many';\n }\n return 'other';\n },\n scottish(n) {\n if (n === 1 || n === 11) {\n return 'one';\n }\n if (n === 2 || n === 12) {\n return 'two';\n }\n if (n >= 3 && n <= 10 || n >= 13 && n <= 19) {\n return 'few';\n }\n return 'other';\n },\n spanish(n) {\n if (n === 1) {\n return 'one';\n }\n if (n % 1000000 === 0 && n !== 0) {\n return 'many';\n }\n return 'other';\n },\n welsh(n) {\n if (n === 0) {\n return 'zero';\n }\n if (n === 1) {\n return 'one';\n }\n if (n === 2) {\n return 'two';\n }\n if (n === 3) {\n return 'few';\n }\n if (n === 6) {\n return 'many';\n }\n return 'other';\n }\n};\n\nexport { I18n };\n//# sourceMappingURL=i18n.mjs.map\n","function closestAttributeValue($element, attributeName) {\n const $closestElementWithAttribute = $element.closest(`[${attributeName}]`);\n return $closestElementWithAttribute ? $closestElementWithAttribute.getAttribute(attributeName) : null;\n}\n\nexport { closestAttributeValue };\n//# sourceMappingURL=closest-attribute-value.mjs.map\n","import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';\nimport { ConfigurableComponent } from '../../common/configuration.mjs';\nimport { formatErrorMessage } from '../../common/index.mjs';\nimport { ElementError } from '../../errors/index.mjs';\nimport { I18n } from '../../i18n.mjs';\n\n/**\n * File upload component\n *\n * @preserve\n * @augments ConfigurableComponent<FileUploadConfig>\n */\nclass FileUpload extends ConfigurableComponent {\n /**\n * @param {Element | null} $root - File input element\n * @param {FileUploadConfig} [config] - File Upload config\n */\n constructor($root, config = {}) {\n super($root, config);\n this.$input = void 0;\n this.$button = void 0;\n this.$status = void 0;\n this.i18n = void 0;\n this.id = void 0;\n this.$announcements = void 0;\n this.enteredAnotherElement = void 0;\n const $input = this.$root.querySelector('input');\n if ($input === null) {\n throw new ElementError({\n component: FileUpload,\n identifier: 'File inputs (`<input type=\"file\">`)'\n });\n }\n if ($input.type !== 'file') {\n throw new ElementError(formatErrorMessage(FileUpload, 'File input (`<input type=\"file\">`) attribute (`type`) is not `file`'));\n }\n this.$input = $input;\n this.$input.setAttribute('hidden', 'true');\n if (!this.$input.id) {\n throw new ElementError({\n component: FileUpload,\n identifier: 'File input (`<input type=\"file\">`) attribute (`id`)'\n });\n }\n this.id = this.$input.id;\n this.i18n = new I18n(this.config.i18n, {\n locale: closestAttributeValue(this.$root, 'lang')\n });\n const $label = this.findLabel();\n if (!$label.id) {\n $label.id = `${this.id}-label`;\n }\n this.$input.id = `${this.id}-input`;\n const $button = document.createElement('button');\n $button.classList.add('govuk-file-upload-button');\n $button.type = 'button';\n $button.id = this.id;\n $button.classList.add('govuk-file-upload-button--empty');\n const ariaDescribedBy = this.$input.getAttribute('aria-describedby');\n if (ariaDescribedBy) {\n $button.setAttribute('aria-describedby', ariaDescribedBy);\n }\n const $status = document.createElement('span');\n $status.className = 'govuk-body govuk-file-upload-button__status';\n $status.setAttribute('aria-live', 'polite');\n $status.innerText = this.i18n.t('noFileChosen');\n $button.appendChild($status);\n const commaSpan = document.createElement('span');\n commaSpan.className = 'govuk-visually-hidden';\n commaSpan.innerText = ', ';\n commaSpan.id = `${this.id}-comma`;\n $button.appendChild(commaSpan);\n const containerSpan = document.createElement('span');\n containerSpan.className = 'govuk-file-upload-button__pseudo-button-container';\n const buttonSpan = document.createElement('span');\n buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload-button__pseudo-button';\n buttonSpan.innerText = this.i18n.t('chooseFilesButton');\n containerSpan.appendChild(buttonSpan);\n containerSpan.insertAdjacentText('beforeend', ' ');\n const instructionSpan = document.createElement('span');\n instructionSpan.className = 'govuk-body govuk-file-upload-button__instruction';\n instructionSpan.innerText = this.i18n.t('dropInstruction');\n containerSpan.appendChild(instructionSpan);\n $button.appendChild(containerSpan);\n $button.setAttribute('aria-labelledby', `${$label.id} ${commaSpan.id} ${$button.id}`);\n $button.addEventListener('click', this.onClick.bind(this));\n $button.addEventListener('dragover', event => {\n event.preventDefault();\n });\n this.$root.insertAdjacentElement('afterbegin', $button);\n this.$input.setAttribute('tabindex', '-1');\n this.$input.setAttribute('aria-hidden', 'true');\n this.$button = $button;\n this.$status = $status;\n this.$input.addEventListener('change', this.onChange.bind(this));\n this.updateDisabledState();\n this.observeDisabledState();\n this.$announcements = document.createElement('span');\n this.$announcements.classList.add('govuk-file-upload-announcements');\n this.$announcements.classList.add('govuk-visually-hidden');\n this.$announcements.setAttribute('aria-live', 'assertive');\n this.$root.insertAdjacentElement('afterend', this.$announcements);\n this.$button.addEventListener('drop', this.onDrop.bind(this));\n document.addEventListener('dragenter', this.updateDropzoneVisibility.bind(this));\n document.addEventListener('dragenter', () => {\n this.enteredAnotherElement = true;\n });\n document.addEventListener('dragleave', () => {\n if (!this.enteredAnotherElement && !this.$button.disabled) {\n this.hideDraggingState();\n this.$announcements.innerText = this.i18n.t('leftDropZone');\n }\n this.enteredAnotherElement = false;\n });\n }\n updateDropzoneVisibility(event) {\n if (this.$button.disabled) return;\n if (event.target instanceof Node) {\n if (this.$root.contains(event.target)) {\n if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {\n if (!this.$button.classList.contains('govuk-file-upload-button--dragging')) {\n this.showDraggingState();\n this.$announcements.innerText = this.i18n.t('enteredDropZone');\n }\n }\n } else {\n if (this.$button.classList.contains('govuk-file-upload-button--dragging')) {\n this.hideDraggingState();\n this.$announcements.innerText = this.i18n.t('leftDropZone');\n }\n }\n }\n }\n showDraggingState() {\n this.$button.classList.add('govuk-file-upload-button--dragging');\n }\n hideDraggingState() {\n this.$button.classList.remove('govuk-file-upload-button--dragging');\n }\n onDrop(event) {\n event.preventDefault();\n if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {\n this.$input.files = event.dataTransfer.files;\n this.$input.dispatchEvent(new CustomEvent('change'));\n this.hideDraggingState();\n }\n }\n onChange() {\n const fileCount = this.$input.files.length;\n if (fileCount === 0) {\n this.$status.innerText = this.i18n.t('noFileChosen');\n this.$button.classList.add('govuk-file-upload-button--empty');\n } else {\n if (fileCount === 1) {\n this.$status.innerText = this.$input.files[0].name;\n } else {\n this.$status.innerText = this.i18n.t('multipleFilesChosen', {\n count: fileCount\n });\n }\n this.$button.classList.remove('govuk-file-upload-button--empty');\n }\n }\n findLabel() {\n const $label = document.querySelector(`label[for=\"${this.$input.id}\"]`);\n if (!$label) {\n throw new ElementError({\n component: FileUpload,\n identifier: `Field label (\\`<label for=${this.$input.id}>\\`)`\n });\n }\n return $label;\n }\n onClick() {\n this.$input.click();\n }\n observeDisabledState() {\n const observer = new MutationObserver(mutationList => {\n for (const mutation of mutationList) {\n if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {\n this.updateDisabledState();\n }\n }\n });\n observer.observe(this.$input, {\n attributes: true\n });\n }\n updateDisabledState() {\n this.$button.disabled = this.$input.disabled;\n this.$root.classList.toggle('govuk-drop-zone--disabled', this.$button.disabled);\n }\n}\nFileUpload.moduleName = 'govuk-file-upload';\nFileUpload.defaults = Object.freeze({\n i18n: {\n chooseFilesButton: 'Choose file',\n dropInstruction: 'or drop file',\n noFileChosen: 'No file chosen',\n multipleFilesChosen: {\n one: '%{count} file chosen',\n other: '%{count} files chosen'\n },\n enteredDropZone: 'Entered drop zone',\n leftDropZone: 'Left drop zone'\n }\n});\nFileUpload.schema = Object.freeze({\n properties: {\n i18n: {\n type: 'object'\n }\n }\n});\nfunction isContainingFiles(dataTransfer) {\n const hasNoTypesInfo = dataTransfer.types.length === 0;\n const isDraggingFiles = dataTransfer.types.some(type => type === 'Files');\n return hasNoTypesInfo || isDraggingFiles;\n}\n\n/**\n * @typedef {HTMLInputElement & {files: FileList}} HTMLFileInputElement\n */\n\n/**\n * File upload config\n *\n * @see {@link FileUpload.defaults}\n * @typedef {object} FileUploadConfig\n * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations\n */\n\n/**\n * File upload translations\n *\n * @see {@link FileUpload.defaults.i18n}\n * @typedef {object} FileUploadTranslations\n *\n * Messages used by the component\n * @property {string} [chooseFile] - The text of the button that opens the file picker\n * @property {string} [dropInstruction] - The text informing users they can drop files\n * @property {TranslationPluralForms} [multipleFilesChosen] - The text displayed when multiple files\n * have been chosen by the user\n * @property {string} [noFileChosen] - The text to displayed when no file has been chosen by the user\n * @property {string} [enteredDropZone] - The text announced by assistive technology\n * when user drags files and enters the drop zone\n * @property {string} [leftDropZone] - The text announced by assistive technology\n * when user drags files and leaves the drop zone without dropping\n */\n\n/**\n * @import { Schema } from '../../common/configuration.mjs'\n * @import { TranslationPluralForms } from '../../i18n.mjs'\n */\n\nexport { FileUpload };\n//# sourceMappingURL=file-upload.mjs.map\n","import { DigitalMarketplaceFrontendComponent } from '../../digitalmarketplace-frontend-component'\nimport { FileUpload } from 'govuk-frontend/dist/govuk/all.mjs'\n\n\nclass ComplianceCommunicationAttachmentListRowFileInput {\n $dropZone: JQuery<HTMLElement>\n $input: JQuery<HTMLInputElement>\n $label: JQuery<HTMLLabelElement>\n $hint: JQuery<HTMLElement>\n $button: JQuery<HTMLButtonElement>\n $comma: JQuery<HTMLElement>\n $announcements: JQuery<HTMLElement>\n\n constructor ($row: JQuery<HTMLElement>, column: number) {\n const $column = $row.find(`.govuk-grid-column-one-half:nth-of-type(${column})`)\n\n this.$dropZone = $column.find('.govuk-drop-zone')\n this.$input = $column.find('input')\n this.$label = $column.find<HTMLLabelElement>('.govuk-label')\n this.$hint = $column.find('div.govuk-hint')\n this.$button = $column.find('button')\n this.$comma = $column.find('button > span.govuk-visually-hidden')\n this.$announcements = $column.find('span.govuk-file-upload-announcements')\n }\n\n updateIndex (newIndex: number) {\n const newName = (this.$input.attr('name') as string).replace(/\\d+/, newIndex.toString())\n const newId = `input-${newName}`\n\n if (this.$button.length) {\n this.$label.attr('for', newId)\n this.$label.attr('id', `${newId}-label`)\n \n this.$hint.attr('id', `${newId}-hint`)\n \n this.$input.attr('id', `${newId}-input`)\n this.$input.attr('name', newName)\n this.$input.attr('aria-describedby', `${newId}-hint`)\n \n this.$button.attr('id', newId)\n this.$button.attr('aria-describedby', `${newId}-hint`)\n this.$button.attr('aria-labelledby', `${newId}-label ${newId}-comma ${newId}`)\n \n this.$comma.attr('id', `${newId}-comma`)\n } else {\n this.$label.attr('for', newId)\n this.$hint.attr('id', `${newId}-hint`)\n \n this.$input.attr('id', newId)\n this.$input.attr('name', newName)\n this.$input.attr('aria-describedby', `${newId}-hint`)\n }\n }\n\n makeInteractable () {\n if (!this.$button.length) {\n return new FileUpload(this.$dropZone.get(0))\n }\n }\n\n resetHtml () {\n this.$label.removeAttr('id')\n \n this.$dropZone.removeAttr('data-govuk-file-upload-init')\n \n this.$button.remove()\n \n const id = this.$input.attr('id') as string\n\n this.$input.removeAttr('hidden')\n this.$input.removeAttr('aria-hidden')\n this.$input.attr('id', id.substring(0, id.length - 6))\n\n this.$announcements.remove()\n }\n}\n\nclass ComplianceCommunicationAttachmentListRequiredInput {\n $requiredSection: JQuery<HTMLElement>\n $input: JQuery<HTMLInputElement>\n $label: JQuery<HTMLLabelElement>\n\n constructor ($row: JQuery<HTMLElement>) {\n this.$requiredSection = $row.find('.dm-compliance-communication__attachments-item-required')\n this.$input = this.$requiredSection.find<HTMLInputElement>('.govuk-checkboxes__item > input')\n this.$label = this.$requiredSection.find<HTMLLabelElement>('.govuk-checkboxes__item > .govuk-label')\n }\n\n isChecked () {\n return this.$input.is(':checked')\n }\n\n updateIndex (newIndex: number) {\n const newName = (this.$input.attr('name') as string).replace(/\\d+/, newIndex.toString())\n const newId = `input-${newName}`\n\n this.$label.attr('for', newId)\n\n this.$input.attr('id', newId)\n this.$input.attr('name', newName)\n }\n\n checkRow () {\n this.$requiredSection.toggleClass('govuk-visually-hidden', true)\n this.$input.prop('checked', true)\n this.$input.attr('tabindex', -1)\n this.$input.attr('tabindex', -1)\n }\n}\n\nclass ComplianceCommunicationAttachmentListRowRemoveButton {\n attachmentListRow: ComplianceCommunicationAttachmentListRow\n $button: JQuery<HTMLElement>\n $removeButtonCounter: JQuery<HTMLElement>\n\n constructor (attachmentListRow: ComplianceCommunicationAttachmentListRow, $row: JQuery<HTMLElement>) {\n this.attachmentListRow = attachmentListRow\n this.$button = $row.find('.dm-compliance-communication__attachments-item-remove')\n this.$removeButtonCounter = this.$button.find('.dm-compliance-communication__attachments-item-remove-counter')\n }\n\n init () {\n this.toggleVisibility(true)\n\n this.$button.on('click', () => {\n this.attachmentListRow.removeRow()\n })\n }\n\n toggleVisibility (isShown: boolean) {\n this.$button.toggleClass('govuk-visually-hidden', !isShown)\n\n if (isShown) {\n this.$button.removeAttr('tabindex')\n } else {\n this.$button.attr('tabindex', -1)\n }\n }\n\n updateIndex (newIndex: number) {\n this.$removeButtonCounter.text(newIndex + 1)\n }\n}\n\nclass ComplianceCommunicationAttachmentListRow {\n $row: JQuery<HTMLElement>\n attachmentList: ComplianceCommunicationAttachments\n index: number\n $legendCounter: JQuery<HTMLElement>\n attachmentListRowFileInput: ComplianceCommunicationAttachmentListRowFileInput\n attachmentListRowRequiredInput: ComplianceCommunicationAttachmentListRequiredInput\n attachmentListRowRemoveButton: ComplianceCommunicationAttachmentListRowRemoveButton\n\n constructor (attachmentList: ComplianceCommunicationAttachments, index: number, $row: JQuery<HTMLElement>) {\n this.$row = $row\n this.attachmentList = attachmentList\n this.index = index\n this.$legendCounter = this.$row.find('.dm-compliance-communication__attachments-item-legend-counter')\n this.attachmentListRowFileInput = new ComplianceCommunicationAttachmentListRowFileInput($row, 1)\n this.attachmentListRowRequiredInput = new ComplianceCommunicationAttachmentListRequiredInput($row)\n this.attachmentListRowRemoveButton = new ComplianceCommunicationAttachmentListRowRemoveButton(this, $row)\n }\n\n init () {\n this.attachmentListRowRemoveButton.init()\n this.attachmentListRowFileInput.makeInteractable()\n this.attachmentListRowRequiredInput.checkRow()\n }\n\n updateIndex (newIndex: number) {\n this.index = newIndex\n this.attachmentListRowFileInput.updateIndex(newIndex)\n this.attachmentListRowRequiredInput.updateIndex(newIndex)\n this.attachmentListRowRemoveButton.updateIndex(newIndex)\n this.$legendCounter.text(newIndex + 1)\n }\n\n removeRow () {\n this.attachmentList.removeRow(this.index)\n this.deleteRow()\n }\n\n deleteRow () {\n this.$row.remove()\n }\n\n toggleRemoveButtonVisibility (isShown: boolean) {\n this.attachmentListRowRemoveButton.toggleVisibility(isShown)\n }\n\n isRowChecked () {\n return this.attachmentListRowRequiredInput.isChecked()\n }\n}\n\nclass ComplianceCommunicationAttachmentListAddButton {\n attachmentList: ComplianceCommunicationAttachments\n $button: JQuery<HTMLButtonElement>\n $buttonCounter: JQuery<HTMLElement>\n\n constructor (attachmentList: ComplianceCommunicationAttachments, $button: JQuery<HTMLButtonElement>) {\n this.attachmentList = attachmentList\n this.$button = $button\n this.$buttonCounter = $button.find('.dm-compliance-communication__attachments__js-remaining-counter')\n }\n\n init (numberOfRows: number) {\n this.$button.on('click', () => {\n this.attachmentList.addRow()\n })\n\n this.updateButtonText(this.attachmentList.maxNumberOfAttachments - numberOfRows)\n }\n\n updateButtonText (numberRemaining: number) {\n this.$buttonCounter.text(numberRemaining)\n }\n\n toggleVisibility (isShown: boolean) {\n this.$button.toggleClass('govuk-visually-hidden', !isShown)\n\n if (isShown) {\n this.$button.removeAttr('tabindex')\n } else {\n this.$button.attr('tabindex', -1)\n }\n }\n}\n\nclass ComplianceCommunicationAttachments implements DigitalMarketplaceFrontendComponent {\n static moduleName = 'dm-compliance-communication-attachments'\n $attachmentList: JQuery<HTMLElement>\n $attachmentListItems: JQuery<HTMLElement>\n attachmentListRows: ComplianceCommunicationAttachmentListRow[]\n attachmentListTemplateRow: ComplianceCommunicationAttachmentListRow\n attachmentListAddButton: ComplianceCommunicationAttachmentListAddButton\n maxNumberOfAttachments: number\n\n constructor ($attachmentList: JQuery<HTMLElement>) {\n this.$attachmentList = $attachmentList\n this.$attachmentListItems = this.$attachmentList.find('.dm-compliance-communication__attachments-items')\n this.attachmentListRows = this.$attachmentListItems.find('.dm-compliance-communication__attachments-item').get().map((row, index) => new ComplianceCommunicationAttachmentListRow(this, index, $(row)))\n this.attachmentListTemplateRow = new ComplianceCommunicationAttachmentListRow(this, 99, this.$attachmentList.find('.dm-compliance-communication__attachments-item-template > .dm-compliance-communication__attachments-item'))\n this.attachmentListAddButton = new ComplianceCommunicationAttachmentListAddButton(this, this.$attachmentList.find<HTMLButtonElement>('.dm-compliance-communication__attachments-add'))\n this.maxNumberOfAttachments = this.$attachmentList.data('maxNumberOfAttachments')\n }\n\n init () {\n const rowsChecked = this.attachmentListRows.map((attachmentListRow) => attachmentListRow.isRowChecked())\n let indexToDeleteFrom\n\n if (rowsChecked.every((rowChecked) => !rowChecked)) {\n indexToDeleteFrom = 0\n } else {\n indexToDeleteFrom = rowsChecked.indexOf(false)\n }\n\n if (indexToDeleteFrom >= 0) {\n this.attachmentListRows.slice(indexToDeleteFrom).forEach((attachmentListRow) => attachmentListRow.deleteRow())\n this.attachmentListRows.splice(indexToDeleteFrom)\n }\n\n this.attachmentListAddButton.init(this.attachmentListRows.length)\n this.attachmentListRows.forEach((attachmentListRow) => attachmentListRow.init())\n\n this.attachmentListTemplateRow.attachmentListRowFileInput.resetHtml()\n\n this.updateIfMinRows()\n this.updateAttachmentButton()\n }\n\n removeRow (indexToRemove: number) {\n this.attachmentListRows.splice(indexToRemove, 1)\n\n this.attachmentListRows.slice(indexToRemove).forEach((attachmentListRow, index) => {\n attachmentListRow.updateIndex(indexToRemove + index)\n })\n\n this.updateIfMinRows()\n this.updateAttachmentButton()\n\n if (indexToRemove > 0) {\n this.focusOnRow(indexToRemove)\n } else {\n this.attachmentListAddButton.$button.focus()\n }\n }\n\n addRow () {\n const numberOfRows = this.attachmentListRows.length\n\n if (numberOfRows < this.maxNumberOfAttachments) {\n const $newRow = this.attachmentListTemplateRow.$row.clone()\n const attachmentListRow = new ComplianceCommunicationAttachmentListRow(this, numberOfRows, $newRow)\n\n attachmentListRow.updateIndex(numberOfRows)\n\n this.attachmentListRows.push(attachmentListRow)\n this.$attachmentListItems.append($newRow)\n this.attachmentListRows[numberOfRows].init()\n\n this.updateIfMinRows()\n this.updateAttachmentButton()\n this.focusOnRow(numberOfRows)\n }\n }\n\n updateIfMinRows () {\n if (this.attachmentListRows.length !== 0) {\n this.attachmentListRows[0].toggleRemoveButtonVisibility(true)\n }\n }\n\n updateAttachmentButton () {\n if (this.attachmentListRows.length === this.maxNumberOfAttachments) {\n this.attachmentListAddButton.toggleVisibility(false)\n } else {\n this.attachmentListAddButton.toggleVisibility(true)\n this.attachmentListAddButton.updateButtonText(this.maxNumberOfAttachments - this.attachmentListRows.length)\n }\n }\n\n focusOnRow (targetIndexToFocus: number) {\n const numberOfRows = this.attachmentListRows.length\n let indexToFocus = targetIndexToFocus\n\n if (targetIndexToFocus >= numberOfRows) {\n indexToFocus = numberOfRows - 1\n }\n\n this.attachme