diff --git a/.babelrc b/.babelrc index 3cdb872137..079641a4e4 100755 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { "presets": ["stage-2", "babel-preset-react", "babel-preset-es2015"], - "plugins": ["transform-class-properties"] + "plugins": ["transform-class-properties", "plugin-transform-runtime"] } diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/abortable-fetch.js b/_prototypes/individual-response--your-household-v15/assets/modules/abortable-fetch.js index a0ced36c67..934cc50209 100644 --- a/_prototypes/individual-response--your-household-v15/assets/modules/abortable-fetch.js +++ b/_prototypes/individual-response--your-household-v15/assets/modules/abortable-fetch.js @@ -1,30 +1,26 @@ -export default class AbortableFetch { +class AboratableFetch { constructor(url, options) { this.url = url; - this.options = options; this.controller = new window.AbortController(); - this.status = 'UNSENT'; + this.options = { ...options, signal: this.controller.signal }; + + fetch(url, options).then(response => { + if (response.ok) { + this.thenCallback(response); + } else { + this.catchCallback(response); + } + }); } - send() { - this.status = 'LOADING'; + then(callback) { + this.thenCallback = callback; + return this; + } - return new Promise((resolve, reject) => { - abortableFetch(this.url, { signal: this.controller.signal, ...this.options }) - .then(response => { - if (response.status >= 200 && response.status < 300) { - this.status = 'DONE'; - resolve(response); - } else { - this.status = 'DONE'; - reject(response); - } - }) - .catch(error => { - this.status = 'DONE'; - reject(error); - }); - }); + catch(callback) { + this.catchCallback = callback; + return this; } abort() { @@ -32,18 +28,4 @@ export default class AbortableFetch { } } -function abortableFetch(url, options) { - return window.fetch(url, options) - .then(response => { - if (response.ok) { - return response; - } else { - const error = new Error(response.statusText); - error.response = response; - throw error; - } - }) - .catch(error => { - throw error; - }); -} +export default (url, options) => new AboratableFetch(url, options); diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/abortable-fetch.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/abortable-fetch.js deleted file mode 100644 index a0ced36c67..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/abortable-fetch.js +++ /dev/null @@ -1,49 +0,0 @@ -export default class AbortableFetch { - constructor(url, options) { - this.url = url; - this.options = options; - this.controller = new window.AbortController(); - this.status = 'UNSENT'; - } - - send() { - this.status = 'LOADING'; - - return new Promise((resolve, reject) => { - abortableFetch(this.url, { signal: this.controller.signal, ...this.options }) - .then(response => { - if (response.status >= 200 && response.status < 300) { - this.status = 'DONE'; - resolve(response); - } else { - this.status = 'DONE'; - reject(response); - } - }) - .catch(error => { - this.status = 'DONE'; - reject(error); - }); - }); - } - - abort() { - this.controller.abort(); - } -} - -function abortableFetch(url, options) { - return window.fetch(url, options) - .then(response => { - if (response.ok) { - return response; - } else { - const error = new Error(response.statusText); - error.response = response; - throw error; - } - }) - .catch(error => { - throw error; - }); -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/form-body-from-object.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/form-body-from-object.js deleted file mode 100644 index ac4b357b7d..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/form-body-from-object.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function formBodyFromObject(object) { - return Object.keys(object).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`).join('&'); -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead-helpers.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead-helpers.js deleted file mode 100644 index 800c4e5155..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead-helpers.js +++ /dev/null @@ -1,11 +0,0 @@ -export function sanitiseTypeaheadText(string, sanitisedQueryReplaceChars = [], trimEnd = true) { - let sanitisedString = string.toLowerCase().replace(/\s\s+/g, ' '); - - sanitisedString = trimEnd ? sanitisedString.trim() : sanitisedString.trimStart(); - - sanitisedQueryReplaceChars.forEach(char => { - sanitisedString = sanitisedString.replace(new RegExp(char, 'g'), ''); - }); - - return sanitisedString; -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.component.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.component.js deleted file mode 100644 index 571f762326..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.component.js +++ /dev/null @@ -1,406 +0,0 @@ -import EventEmitter from 'events'; -import {sanitiseTypeaheadText} from './typeahead-helpers'; - -const classTypeaheadCombobox = 'js-typeahead-combobox'; -const classTypeaheadLabel = 'js-typeahead-label'; -const classTypeaheadInput = 'js-typeahead-input'; -const classTypeaheadInstructions = 'js-typeahead-instructions'; -const classTypeaheadListbox = 'js-typeahead-listbox'; -const classTypeaheadAriaStatus = 'js-typeahead-aria-status'; - -const classTypeaheadOption = 'typeahead__option'; -const classTypeaheadOptionFocused = `${classTypeaheadOption}--focused`; -const classTypeaheadOptionNoResults = `${classTypeaheadOption}--no-results`; -const classTypeaheadOptionMoreResults = `${classTypeaheadOption}--more-results`; -const classTypeaheadComboboxFocused = 'typeahead__combobox--focused'; -const classTypeaheadHasResults = 'typeahead--has-results'; - -const KEYCODE = { - BACK_SPACE: 8, - RETURN: 13, - ENTER: 14, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - DELETE: 46, - V: 86 -}; - -export const NEW_FIELD_VALUE_EVENT = 'NEW_FIELD_VALUE'; -export const NEW_ITEM_SELECTED_EVENT = 'NEW_ITEM_SELECTED'; -export const UNSET_FIELD_VALUE_EVENT = 'UNSET_FIELD_VALUE'; - -export default class TypeaheadComponent { - emitter = new EventEmitter(); - - constructor({ - context, - apiUrl, - minChars, - resultLimit, - sanitisedQueryReplaceChars = [], - lang = null}) { - - // DOM Elements - this.context = context; - this.combobox = context.querySelector(`.${classTypeaheadCombobox}`); - this.label = context.querySelector(`.${classTypeaheadLabel}`); - this.input = context.querySelector(`.${classTypeaheadInput}`); - this.listbox = context.querySelector(`.${classTypeaheadListbox}`); - this.instructions = context.querySelector(`.${classTypeaheadInstructions}`); - this.ariaStatus = context.querySelector(`.${classTypeaheadAriaStatus}`); - - // Suggestion URL - this.apiUrl = apiUrl || context.getAttribute('data-api-url'); - - // Settings - this.content = JSON.parse(context.getAttribute('data-content')); - this.listboxId = this.listbox.getAttribute('id'); - this.minChars = minChars || 2; - this.resultLimit = resultLimit || null; - - // State - this.ctrlKey = false; - this.deleting = false; - this.query = ''; - this.sanitisedQuery = ''; - this.previousQuery = ''; - this.results = []; - this.resultOptions = []; - this.foundResults = 0; - this.numberOfResults = 0; - this.highlightedResultIndex = 0; - this.settingResult = false; - this.resultSelected = false; - this.blurring = false; - this.blurTimeout = null; - this.sanitisedQueryReplaceChars = sanitisedQueryReplaceChars; - this.lang = lang || document.documentElement.getAttribute('lang').toLowerCase(); - - // Modify DOM - this.label.setAttribute('for', this.input.getAttribute('id')); - this.input.setAttribute('aria-autocomplete', 'list'); - this.input.setAttribute('aria-controls', this.listbox.getAttribute('id')); - this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id')); - this.input.setAttribute('autocomplete', this.input.getAttribute('data-autocomplete')); - this.context.classList.add('typeahead--initialised'); - - // Bind event listeners - this.bindEventListeners(); - } - - bindEventListeners() { - this.input.addEventListener('keydown', this.handleKeydown.bind(this)); - this.input.addEventListener('keyup', this.handleKeyup.bind(this)); - this.input.addEventListener('input', this.handleChange.bind(this)); - this.input.addEventListener('focus', this.handleFocus.bind(this)); - this.input.addEventListener('blur', this.handleBlur.bind(this)); - - this.listbox.addEventListener('mouseover', this.handleMouseover.bind(this)); - this.listbox.addEventListener('mouseout', this.handleMouseout.bind(this)); - } - - handleKeydown(event) { - this.ctrlKey = ((event.ctrlKey || event.metaKey) && event.keyCode !== KEYCODE.V); - - switch (event.keyCode) { - case KEYCODE.UP: { - event.preventDefault(); - this.navigateResults(-1); - break; - } - case KEYCODE.DOWN: { - event.preventDefault(); - this.navigateResults(1); - break; - } - case KEYCODE.ENTER: - case KEYCODE.RETURN: { - event.preventDefault(); - break; - } - } - } - - handleKeyup(event) { - switch (event.keyCode) { - case KEYCODE.UP: - case KEYCODE.DOWN: { - event.preventDefault(); - break; - } - case KEYCODE.ENTER: - case KEYCODE.RETURN: { - this.selectResult(); - break; - } - case KEYCODE.LEFT: - case KEYCODE.RIGHT: { - break; - } - } - - this.ctrlKey = false; - } - - handleChange() { - if (!this.blurring) { - this.valueChanged(); - } - } - - handleFocus() { - clearTimeout(this.blurTimeout); - this.combobox.classList.add(classTypeaheadComboboxFocused); - } - - handleBlur() { - clearTimeout(this.blurTimeout); - this.blurring = true; - - this.blurTimeout = setTimeout(() => { - this.combobox.classList.remove(classTypeaheadComboboxFocused); - this.blurring = false; - }, 0); - } - - handleMouseover() { - const focusedItem = this.resultOptions[this.highlightedResultIndex]; - - if (focusedItem) { - focusedItem.classList.remove(classTypeaheadOptionFocused); - } - } - - handleMouseout() { - const focusedItem = this.resultOptions[this.highlightedResultIndex]; - - if (focusedItem) { - focusedItem.classList.add(classTypeaheadOptionFocused); - } - } - - navigateResults(direction) { - let index = 0; - - if (this.highlightedResultIndex !== null) { - index = this.highlightedResultIndex + direction; - } - - if (index < this.numberOfResults) { - if (index < 0) { - index = null; - } - - this.setHighlightedResult(index); - } - } - - valueChanged(force) { - if (!this.settingResult) { - const query = this.input.value; - const sanitisedQuery = sanitiseTypeaheadText(query, this.sanitisedQueryReplaceChars); - - if (sanitisedQuery !== this.sanitisedQuery || (force && !this.resultSelected)) { - this.unsetResults(); - this.setAriaStatus(); - - this.query = query; - this.sanitisedQuery = sanitisedQuery; - - if (this.sanitisedQuery.length >= this.minChars) { - this.emitter.emit(NEW_FIELD_VALUE_EVENT, sanitisedQuery); - } else { - this.clearListbox(); - } - } - } - } - - unsetResults() { - this.results = []; - this.resultOptions = []; - this.resultSelected = false; - - this.emitter.emit(UNSET_FIELD_VALUE_EVENT); - } - - clearListbox(preventAriaStatusUpdate) { - this.listbox.innerHTML = ''; - this.context.classList.remove(classTypeaheadHasResults); - this.input.removeAttribute('aria-activedescendant'); - this.combobox.removeAttribute('aria-expanded'); - - if (!preventAriaStatusUpdate) { - this.setAriaStatus(); - } - } - - updateData(dataMap) { - this.results = dataMap.results; - this.foundResults = dataMap.totalResults; - this.numberOfResults = Math.max(this.results.length, 0); - - this.render(); - } - - render() { - if (!this.deleting || (this.numberOfResults && this.deleting)) { - if (this.numberOfResults.length === 1 && this.results[0].sanitisedText === this.sanitisedQuery) { - this.clearListbox(true); - this.selectResult(0); - } else { - this.listbox.innerHTML = ''; - this.resultOptions = this.results.map((result, index) => { - let ariaLabel = result[this.lang]; - let innerHTML = this.emboldenMatch(ariaLabel, this.query); - - if (Array.isArray(result.sanitisedAlternatives)) { - const alternativeMatch = result.sanitisedAlternatives.find(alternative => alternative !== result.sanitisedText && alternative.includes(this.sanitisedQuery)); - - if (alternativeMatch) { - const alternativeText = result.alternatives[result.sanitisedAlternatives.indexOf(alternativeMatch)]; - innerHTML += ` (${this.emboldenMatch(alternativeText, this.query)})`; - ariaLabel += `, (${alternativeText})`; - } - } - - const listElement = document.createElement('li'); - listElement.className = classTypeaheadOption; - listElement.setAttribute('id', `${this.listboxId}__option--${index}`); - listElement.setAttribute('role', 'option'); - listElement.setAttribute('tabindex', '-1'); - listElement.setAttribute('aria-label', ariaLabel); - listElement.innerHTML = innerHTML; - - listElement.addEventListener('click', () => { - this.selectResult(index); - }); - - return listElement; - }); - - this.resultOptions.forEach(listElement => this.listbox.appendChild(listElement)); - - if (this.numberOfResults < this.foundResults) { - const listElement = document.createElement('li'); - listElement.className = `${classTypeaheadOption} ${classTypeaheadOptionMoreResults} u-fs-b`; - listElement.setAttribute('tabindex', '-1'); - listElement.setAttribute('aria-hidden', 'true'); - listElement.innerHTML = this.content.more_results; - this.listbox.appendChild(listElement); - } - - this.setHighlightedResult(null); - this.combobox.setAttribute('aria-expanded', true); - this.context.classList.add(classTypeaheadHasResults); - } - } - - if (this.numberOfResults === 0 && this.content.no_results) { - this.listbox.innerHTML = `
  • ${this.content.no_results}
  • `; - this.combobox.setAttribute('aria-expanded', true); - this.context.classList.add(classTypeaheadHasResults); - } - } - - setHighlightedResult(index) { - this.highlightedResultIndex = index; - - if (this.setHighlightedResult === null) { - this.input.removeAttribute('aria-activedescendant'); - } else if (this.numberOfResults) { - this.resultOptions.forEach((option, optionIndex) => { - if (optionIndex === index) { - option.classList.add(classTypeaheadOptionFocused); - option.setAttribute('aria-selected', true); - this.input.setAttribute('aria-activedescendant', option.getAttribute('id')); - } else { - option.classList.remove(classTypeaheadOptionFocused); - option.removeAttribute('aria-selected'); - } - }); - - this.setAriaStatus(); - } - } - - setAriaStatus(content) { - if (!content) { - const queryTooShort = this.sanitisedQuery.length < this.minChars; - const noResults = this.numberOfResults === 0; - - if (queryTooShort) { - content = this.content.aria_min_chars; - } else if (noResults) { - content = `${this.content.aria_no_results}: "${this.query}"`; - } else if (this.numberOfResults === 1) { - content = this.content.aria_one_result; - } else { - content = this.content.aria_n_results.replace('{n}', this.numberOfResults); - - if (this.resultLimit && this.foundResults > this.resultLimit) { - content += ` ${this.content.aria_limited_results}`; - } - } - } - - this.ariaStatus.innerHTML = content; - } - - selectResult(index) { - if (this.results.length) { - //this.settingResult = true; - - const result = this.results[index || this.highlightedResultIndex || 0]; - - // TODO: This condition should be removed if we go with the internal address lookup API, or made configurable if we use a third party API - if (result.type !== 'Postcode') { - this.input.value = result[this.lang]; - this.query = result[this.lang]; - } - - this.resultSelected = true; - - this.emitter.emit(NEW_ITEM_SELECTED_EVENT, result); - - /*this.onSelect(result).then(() => { - this.settingResult = false; - // this.input.setAttribute('autocomplete', 'false'); - });*/ - - let ariaAlternativeMessage = ''; - - if (!result.sanitisedText.includes(this.sanitisedQuery) && result.sanitisedAlternatives) { - const alternativeMatch = result.sanitisedAlternatives.find(alternative => alternative.includes(this.sanitisedQuery)); - - if (alternativeMatch) { - ariaAlternativeMessage = `, ${this.content.aria_found_by_alternative_name}: ${alternativeMatch}`; - } - } - - const ariaMessage = `${this.content.aria_you_have_selected}: ${result[this.lang]}${ariaAlternativeMessage}.`; - - this.clearListbox(); - this.setAriaStatus(ariaMessage); - } - } - - emboldenMatch(string, query) { - query = query.toLowerCase().trim(); - - if (string.toLowerCase().includes(query)) { - const queryLength = query.length; - const matchIndex = string.toLowerCase().indexOf(query); - const matchEnd = matchIndex + queryLength; - const before = string.substr(0, matchIndex); - const match = string.substr(matchIndex, queryLength); - const after = string.substr(matchEnd, string.length - matchEnd); - - return `${before}${match}${after}`; - } else { - return string; - } - } -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.container.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.container.js deleted file mode 100644 index f4b4371243..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.container.js +++ /dev/null @@ -1,44 +0,0 @@ -import TypeaheadComponent, { - NEW_FIELD_VALUE_EVENT, - NEW_ITEM_SELECTED_EVENT, - UNSET_FIELD_VALUE_EVENT -} from './typeahead.component'; -import TypeaheadService from './typeahead.service'; -import typeaheadDataMap from './typeahead.service-data-map'; - -export default class TypeaheadContainer { - constructor(context) { - this.typeahead = new TypeaheadComponent({ context }); - - this.service = new TypeaheadService({ - apiUrl: this.typeahead.apiUrl, - lang: document.documentElement.getAttribute('lang').toLowerCase(), - sanitisedQueryReplaceChars: this.typeahead.sanitisedQueryReplaceChars - }); - - this.typeahead.emitter.on(NEW_FIELD_VALUE_EVENT, value => { - - /** - * Call service, partially apply config for promise callbacks - */ - this.service.get(value) - .then(typeaheadDataMap.bind(null, { - query: value, - lang: this.typeahead.lang, - sanitisedQueryReplaceChars: this.typeahead.sanitisedQueryReplaceChars - })) - .then(this.typeahead.updateData.bind(this.typeahead)) - .catch(error => { - if (error.name !== 'AbortError') { - console.log('TypeaheadService error: ', error, 'query: ', value); - } - }); - }); - - this.code = context.querySelector('.js-typeahead-code'); - - this.typeahead.emitter.on(NEW_ITEM_SELECTED_EVENT, value => (this.code.value = value.code)); - - this.typeahead.emitter.on(UNSET_FIELD_VALUE_EVENT, () => (this.code.value = '')); - } -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.css b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.css deleted file mode 100644 index 64925be718..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.css +++ /dev/null @@ -1,114 +0,0 @@ -.typeahead { - position: relative -} - -.typeahead__combobox { - display: inline-block; - border-radius: 3px -} - -.typeahead__combobox--focused { - -webkit-box-shadow: 0 0 0 3px #fe781f; - box-shadow: 0 0 0 3px #fe781f -} - -.typeahead__preview { - position: absolute; - width: 100%; - border-color: transparent; - color: #595959; - pointer-events: none -} - -.typeahead__listbox { - margin: 0; - padding: 0; - width: 100%; - list-style: none; - border: 1px solid #999; - border-top: 0; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px -} - -@media only screen and (min-width: 500px) { - .typeahead__listbox:not(.typeahead__listbox--full-width) { - width: 19.5rem - } -} - -.typeahead__listbox:empty { - display: none -} - -.typeahead__option { - margin: 0; - padding: .5rem; - outline: none; - cursor: pointer -} - -.typeahead__option:not(:last-child) { - border-bottom: 1px solid #999 -} - -.typeahead__option--focused:not(.typeahead__option--no-results), .typeahead__option:not(.typeahead__option--no-results):not(.typeahead__option--more-results):hover { - border-color: #4263c2; - background: #4263c2; - color: #fff -} - -.typeahead__option--more-results, .typeahead__option--no-results { - background: #ccc; - cursor: not-allowed -} - -.typeahead__combobox--focused .typeahead__listbox { - border-color: #999 -} - -.typeahead:not(.typeahead--initialised) .typeahead__instructions, .typeahead:not(.typeahead--initialised) .typeahead__listbox, .typeahead:not(.typeahead--initialised) .typeahead__preview, .typeahead:not(.typeahead--initialised) .typeahead__status { - display: none -} - -.typeahead--initialised .typeahead__input { - background: transparent -} - -.typeahead--initialised .typeahead__input:focus { - -webkit-box-shadow: none; - box-shadow: none -} - -.typeahead--has-results .typeahead__input { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0 -} - -.address-input { - max-width: 580px -} - -.address-input--search .address-input__manual, .address-input__search { - display: none -} - -.address-input--search .address-input__search { - display: block -} - -.address-input__typeahead .typeahead__combobox .typeahead__listbox { - width: 100% -} - -.address-input__typeahead .typeahead__combobox { - display: block -} - -.previous-link { - line-height: 1 -} - -.field__description { - margin-top: 1rem -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.module.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.module.js deleted file mode 100644 index 05075901a5..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.module.js +++ /dev/null @@ -1,12 +0,0 @@ -import TypeaheadContainer from './typeahead.container'; - -function TypeaheadModule() { - const typeaheads = [...document.querySelectorAll('.js-typeahead')]; - - typeaheads.forEach(typeahead => new TypeaheadContainer(typeahead)); -} - -/** - * Temporary - just for prototype, should belong in main/boot file - */ -document.addEventListener('TYPEAHEAD-READY', () => new TypeaheadModule()); diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service-data-map.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service-data-map.js deleted file mode 100644 index 8473e98b22..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service-data-map.js +++ /dev/null @@ -1,35 +0,0 @@ -import {sanitiseTypeaheadText} from './typeahead-helpers'; - -export default function typeaheadDataMap(opts, response) { - - /** - * Required parameter validation needed - */ - - return response.json() - .then(data => { - const results = data.results; - - results.forEach(result => { - result.sanitisedText = sanitiseTypeaheadText(result[opts.lang], opts.sanitisedQueryReplaceChars); - - if (opts.lang !== 'en-gb') { - const english = result['en-gb']; - const sanitisedAlternative = sanitiseTypeaheadText(english, this.sanitisedQueryReplaceChars); - - if (sanitisedAlternative.match(opts.query)) { - result.alternatives = [english]; - result.sanitisedAlternatives = [sanitisedAlternative]; - } - } else { - result.alternatives = []; - result.sanitisedAlternatives = []; - } - }); - - return { - results, - totalResults: data.totalResults - }; - }); -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service.js deleted file mode 100644 index 9ed7f8937e..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead-refactored/typeahead.service.js +++ /dev/null @@ -1,42 +0,0 @@ -import formBodyFromObject from './form-body-from-object'; -import AbortableFetch from './abortable-fetch'; - -export default class TypeaheadService { - requestConfig = { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }; - - constructor({ apiUrl, lang }) { - if (!apiUrl || !lang) { - throw Error( - '[TypeaheadService] \'apiUrl\', \'lang\' parameters are required' - ); - } - - this.apiUrl = apiUrl; - this.lang = lang; - } - - get(sanitisedQuery) { - return new Promise((resolve, reject) => { - const query = { - query: sanitisedQuery, - lang: this.lang - }; - - if (this.fetch && this.fetch.status !== 'DONE') { - this.fetch.abort(); - } - - this.requestConfig.body = formBodyFromObject(query); - this.fetch = new AbortableFetch(this.apiUrl, this.requestConfig); - - this.fetch.send() - .then(resolve) - .catch(reject); - }); - } -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-results.css b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-results.css new file mode 100644 index 0000000000..ea9f589abf --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-results.css @@ -0,0 +1,52 @@ +.typeahead-input { + position: relative; +} +.typeahead-input__combobox { + display: inline-block; + border-radius: 3px; +} +.typeahead-input__results { + display: none; + margin: 0.5rem 0 0; + padding: 0; + width: 100%; + overflow: hidden; + border: 1px solid #222; + border-radius: 3px; +} +.typeahead-input__results-title { + padding: 0.25rem 0.5rem; + border-bottom: 1px solid #222; + background: #d0d0d0; +} +.typeahead-input__listbox { + margin: 0; + padding: 0; + list-style: none; + background: #fff; +} +.typeahead-input__option { + margin: 0; + padding: 0.5rem; + outline: none; + cursor: pointer; +} +.typeahead-input__option:not(:last-child) { + border-bottom: 1px solid #222; +} +.typeahead-input__option self:not(.typeahead-input__option--no-results):not(.typeahead-input__option--more-results):hover, .typeahead-input__option self--focused:not(.typeahead-input__option--no-results) { + border-color: #4263c2; + background: #4263c2; + color: #fff; +} +.typeahead-input__option--no-results, .typeahead-input__option--more-results { + padding: 0.25rem 0.5rem; + background: #d0d0d0; + cursor: not-allowed; +} +.typeahead-input self:not(.typeahead-input--initialised)self__instructions, .typeahead-input self:not(.typeahead-input--initialised)self__listbox, .typeahead-input self:not(.typeahead-input--initialised)self__status { + display: none; +} +.typeahead-input--has-results .typeahead-input__results { + display: block; +} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-type.css b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-type.css new file mode 100644 index 0000000000..39812706f7 --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input-type.css @@ -0,0 +1,55 @@ +.input-type { + display: block; +} +.input-type__inner { + display: inline-flex; + position: relative; +} +.input-type .input-type__input { + z-index: 1; + flex: 1 1 auto; + position: relative; +} +.input-type .input-type__input:focus { + box-shadow: none; +} +.input-type__type[title] { + display: block; + flex: 0 0 auto; + padding: rem-calc(rem-calc(rems(40px) - 1.444rem) / 2) 1rem; + border: 1px solid #222; + background-color: #f0f0f0; + font-size: 1rem; + font-weight: 600; + line-height: normal; + text-align: center; + text-decoration: none; + white-space: nowrap; +} +.input-type__input:focus + .input-type__type[title]:after { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: 3px; + box-shadow: 0 0 0 3px #fd0; +} +.input-type self:not(.input-type--prefix)self__type[title] { + border-left: 0; + border-radius: 0 3px 3px 0; +} +.input-type self:not(.input-type--prefix)self__input { + border-radius: 3px 0 0 3px; +} +.input-type--prefix .input-type__type[title] { + order: 0; + border-right: 0; + border-radius: 3px 0 0 3px; +} +.input-type--prefix .input-type__input { + order: 1; + border-radius: 0 3px 3px 0; +} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input.css b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input.css new file mode 100644 index 0000000000..0fa26a5617 --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/_input.css @@ -0,0 +1,75 @@ +.input { + z-index: 3; + display: block; + position: relative; + padding: rem-calc(rem-calc(rems(40px) - 1.444rem) / 2) 0.5rem; + width: 100%; + border: 1px solid #222; + border-radius: 3px; + color: inherit; + font-family: inherit; + font-size: 1rem; + line-height: 1rem; +} +.input::-ms-clear { + display: none; +} +.input--text, .input--textarea { + -webkit-appearance: none; +} +.input:focus { + outline: 1px solid #222; + outline-offset: -2px; + box-shadow: 0 0 0 3px #fd0; +} +.input--postcode { + width: 100%; + max-width: input-width-calc(); +} +.input__helper { + margin-top: 0.2rem; + font-size: 0.8rem; + font-weight: 600; +} +.input--select { + appearance: none; + padding: 0.5rem 2rem 0.5rem 0.5rem; + background: #fff url('../img/icons/icons--chevron-down.svg') no-repeat center right 10px; + background-size: 1rem; +} +.input--select::-ms-expand { + display: none; +} +.input--textarea { + width: 100%; + line-height: normal; + resize: vertical; +} +.input--block { + display: block; + width: 100%; +} +.input--placeholder { + background: transparent; +} +.input--placeholder::placeholder { + color: transparent; +} +.input--placeholder:valid { + background: #fff; +} +.input--placeholder:focus { + background: #fff; +} +.input--limit-reached { + border: 1px solid #d0021b; +} +.input--limit-reached:focus { + outline: 1px solid #d0021b; +} +.input__limit { + display: block; +} +.input__limit--reached { + color: #d0021b; +} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/code.list.searcher.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/code.list.searcher.js new file mode 100644 index 0000000000..096618a0be --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/code.list.searcher.js @@ -0,0 +1,14 @@ +const Fuse = require('fuse.js'); + +export default function queryJson(query, data, searchFields) { + const options = { + shouldSort: true, + threshold: 0.2, + location: 0, + distance: 1000, + keys: [searchFields], + }; + const fuse = new Fuse(data, options); + let result = fuse.search(query); + return result; +} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-helpers.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-helpers.js deleted file mode 100644 index 800c4e5155..0000000000 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-helpers.js +++ /dev/null @@ -1,11 +0,0 @@ -export function sanitiseTypeaheadText(string, sanitisedQueryReplaceChars = [], trimEnd = true) { - let sanitisedString = string.toLowerCase().replace(/\s\s+/g, ' '); - - sanitisedString = trimEnd ? sanitisedString.trim() : sanitisedString.trimStart(); - - sanitisedQueryReplaceChars.forEach(char => { - sanitisedString = sanitisedString.replace(new RegExp(char, 'g'), ''); - }); - - return sanitisedString; -} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.css b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.css index 64925be718..09fa23cc1f 100644 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.css +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.css @@ -112,3 +112,191 @@ .field__description { margin-top: 1rem } + +.input { + z-index: 3; + display: block; + position: relative; + padding: rem-calc(rem-calc(rems(40px) - 1.444rem) / 2) 0.5rem; + width: 100%; + border: 1px solid #222; + border-radius: 3px; + color: inherit; + font-family: inherit; + font-size: 1rem; + line-height: 1rem; + } + .input::-ms-clear { + display: none; + } + .input--text, .input--textarea { + -webkit-appearance: none; + } + .input:focus { + outline: 1px solid #222; + outline-offset: -2px; + box-shadow: 0 0 0 3px #fd0; + } + .input--postcode { + width: 100%; + max-width: input-width-calc(); + } + .input__helper { + margin-top: 0.2rem; + font-size: 0.8rem; + font-weight: 600; + } + .input--select { + appearance: none; + padding: 0.5rem 2rem 0.5rem 0.5rem; + background: #fff url('../img/icons/icons--chevron-down.svg') no-repeat center right 10px; + background-size: 1rem; + } + .input--select::-ms-expand { + display: none; + } + .input--textarea { + width: 100%; + line-height: normal; + resize: vertical; + } + .input--block { + display: block; + width: 100%; + } + .input--placeholder { + background: transparent; + } + .input--placeholder::placeholder { + color: transparent; + } + .input--placeholder:valid { + background: #fff; + } + .input--placeholder:focus { + background: #fff; + } + .input--limit-reached { + border: 1px solid #d0021b; + } + .input--limit-reached:focus { + outline: 1px solid #d0021b; + } + .input__limit { + display: block; + } + .input__limit--reached { + color: #d0021b; + } + + + .input-type { + display: block; + } + .input-type__inner { + display: inline-flex; + position: relative; + } + .input-type .input-type__input { + z-index: 1; + flex: 1 1 auto; + position: relative; + } + .input-type .input-type__input:focus { + box-shadow: none; + } + .input-type__type[title] { + display: block; + flex: 0 0 auto; + padding: rem-calc(rem-calc(rems(40px) - 1.444rem) / 2) 1rem; + border: 1px solid #222; + background-color: #f0f0f0; + font-size: 1rem; + font-weight: 600; + line-height: normal; + text-align: center; + text-decoration: none; + white-space: nowrap; + } + .input-type__input:focus + .input-type__type[title]:after { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: 3px; + box-shadow: 0 0 0 3px #fd0; + } + .input-type self:not(.input-type--prefix)self__type[title] { + border-left: 0; + border-radius: 0 3px 3px 0; + } + .input-type self:not(.input-type--prefix)self__input { + border-radius: 3px 0 0 3px; + } + .input-type--prefix .input-type__type[title] { + order: 0; + border-right: 0; + border-radius: 3px 0 0 3px; + } + .input-type--prefix .input-type__input { + order: 1; + border-radius: 0 3px 3px 0; + } + + + .typeahead-input { + position: relative; + } + .typeahead-input__combobox { + display: inline-block; + border-radius: 3px; + } + .typeahead-input__results { + display: none; + margin: 0.5rem 0 0; + padding: 0; + width: 100%; + overflow: hidden; + border: 1px solid #222; + border-radius: 3px; + } + .typeahead-input__results-title { + padding: 0.25rem 0.5rem; + border-bottom: 1px solid #222; + background: #d0d0d0; + } + .typeahead-input__listbox { + margin: 0; + padding: 0; + list-style: none; + background: #fff; + } + .typeahead-input__option { + margin: 0; + padding: 0.5rem; + outline: none; + cursor: pointer; + } + .typeahead-input__option:not(:last-child) { + border-bottom: 1px solid #222; + } + .typeahead-input__option self:not(.typeahead-input__option--no-results):not(.typeahead-input__option--more-results):hover, .typeahead-input__option self--focused:not(.typeahead-input__option--no-results) { + border-color: #4263c2; + background: #4263c2; + color: #fff; + } + .typeahead-input__option--no-results, .typeahead-input__option--more-results { + padding: 0.25rem 0.5rem; + background: #d0d0d0; + cursor: not-allowed; + } + .typeahead-input self:not(.typeahead-input--initialised)self__instructions, .typeahead-input self:not(.typeahead-input--initialised)self__listbox, .typeahead-input self:not(.typeahead-input--initialised)self__status { + display: none; + } + .typeahead-input--has-results .typeahead-input__results { + display: block; + } + \ No newline at end of file diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.dom.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.dom.js new file mode 100644 index 0000000000..5f19642af6 --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.dom.js @@ -0,0 +1,13 @@ +import domready from '../../../../../_js/domready'; + +async function initialiseTypeaheads() { + const typeaheads = [...document.querySelectorAll('.js-typeahead')]; + + if (typeaheads.length) { + const Typeahead = (await import('./typeahead')).default; + + typeaheads.forEach(typeahead => new Typeahead(typeahead)); + } +} + +domready(initialiseTypeaheads); diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.helpers.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.helpers.js new file mode 100644 index 0000000000..d855f41dac --- /dev/null +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.helpers.js @@ -0,0 +1,13 @@ +export function sanitiseTypeaheadText(string, sanitisedQueryRemoveChars = [], trimEnd = true) { + let sanitisedString = string.toLowerCase(); + + sanitisedQueryRemoveChars.forEach(char => { + sanitisedString = sanitisedString.replace(new RegExp(char.toLowerCase(), 'g'), ''); + }); + + sanitisedString = sanitisedString.replace(/\s\s+/g, ' '); + + sanitisedString = trimEnd ? sanitisedString.trim() : sanitisedString.trimStart(); + + return sanitisedString; +} diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.js index 96a9c1f85d..5ca7e6c1cf 100644 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.js +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.js @@ -1,27 +1,33 @@ -import TypeaheadCore from './typeahead-core'; +import TypeaheadUI from './typeahead.ui'; -class Typeahead { +export default class Typeahead { constructor(context) { this.context = context; - this.typeahead = new TypeaheadCore({ + this.lang = document.documentElement.getAttribute('lang').toLowerCase(); + this.typeahead = new TypeaheadUI({ context, + lang: this.lang, onSelect: this.onSelect.bind(this), onUnsetResult: this.onUnsetResult.bind(this), + onError: this.onError.bind(this), }); - - this.code = context.querySelector('.js-typeahead-code'); } onSelect(result) { return new Promise(resolve => { - this.code.value = result.code; - + this.typeahead.input.value = result.displayText; resolve(); }); } onUnsetResult() { - this.code.value = ''; + return new Promise(resolve => { + resolve(); + }); + } + + onError(error) { + console.error(error); } } diff --git a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-core.js b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.ui.js similarity index 52% rename from _prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-core.js rename to _prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.ui.js index 8e9dd15a8a..d1f1f2e07a 100644 --- a/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead-core.js +++ b/_prototypes/individual-response--your-household-v15/assets/modules/typeahead/typeahead.ui.js @@ -1,58 +1,70 @@ -import AbortableFetch from '../abortable-fetch'; -import formBodyFromObject from '../form-body-from-object'; -import {sanitiseTypeaheadText} from './typeahead-helpers'; - -const classTypeaheadCombobox = 'js-typeahead-combobox'; -const classTypeaheadLabel = 'js-typeahead-label'; -const classTypeaheadInput = 'js-typeahead-input'; -const classTypeaheadInstructions = 'js-typeahead-instructions'; -const classTypeaheadListbox = 'js-typeahead-listbox'; -const classTypeaheadAriaStatus = 'js-typeahead-aria-status'; - -const classTypeaheadOption = 'typeahead__option'; -const classTypeaheadOptionFocused = `${classTypeaheadOption}--focused`; -const classTypeaheadOptionNoResults = `${classTypeaheadOption}--no-results`; -const classTypeaheadOptionMoreResults = `${classTypeaheadOption}--more-results`; -const classTypeaheadComboboxFocused = 'typeahead__combobox--focused'; -const classTypeaheadHasResults = 'typeahead--has-results'; - -const KEYCODE = { - BACK_SPACE: 8, - RETURN: 13, - ENTER: 14, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - DELETE: 46, - V: 86 -}; - -export default class TypeaheadCore { - constructor({context, apiUrl, onSelect, onUnsetResult, onError, minChars, resultLimit, sanitisedQueryReplaceChars = [], suggestionFunction}) { +import { sortBy } from 'sort-by-typescript'; + +import queryJson from './code.list.searcher'; +import { sanitiseTypeaheadText } from './typeahead.helpers'; +import fetch from '../abortable-fetch'; + +export const baseClass = 'js-typeahead'; + +export const classTypeaheadOption = 'typeahead-input__option'; +export const classTypeaheadOptionFocused = `${classTypeaheadOption}--focused`; +export const classTypeaheadOptionNoResults = `${classTypeaheadOption}--no-results u-fs-s`; +export const classTypeaheadOptionMoreResults = `${classTypeaheadOption}--more-results u-fs-s`; +export const classTypeaheadHasResults = 'typeahead-input--has-results'; + +export default class TypeaheadUI { + constructor({ + context, + typeaheadData, + sanitisedQueryReplaceChars, + minChars, + resultLimit, + suggestOnBoot, + onSelect, + onError, + onUnsetResult, + suggestionFunction, + lang, + ariaYouHaveSelected, + ariaMinChars, + ariaOneResult, + ariaNResults, + ariaLimitedResults, + moreResults, + resultsTitle, + noResults, + }) { // DOM Elements this.context = context; - this.combobox = context.querySelector(`.${classTypeaheadCombobox}`); - this.label = context.querySelector(`.${classTypeaheadLabel}`); - this.input = context.querySelector(`.${classTypeaheadInput}`); - this.listbox = context.querySelector(`.${classTypeaheadListbox}`); - this.instructions = context.querySelector(`.${classTypeaheadInstructions}`); - this.ariaStatus = context.querySelector(`.${classTypeaheadAriaStatus}`); + this.input = context.querySelector(`.${baseClass}-input`); + this.resultsContainer = context.querySelector(`.${baseClass}-results`); + this.listbox = this.resultsContainer.querySelector(`.${baseClass}-listbox`); + this.instructions = context.querySelector(`.${baseClass}-instructions`); + this.ariaStatus = context.querySelector(`.${baseClass}-aria-status`); - // Suggestion URL - this.apiUrl = apiUrl || context.getAttribute('data-api-url'); + // Settings + this.typeaheadData = typeaheadData || context.getAttribute('data-typeahead-data'); + + this.ariaYouHaveSelected = ariaYouHaveSelected || context.getAttribute('data-aria-you-have-selected'); + this.ariaMinChars = ariaMinChars || context.getAttribute('data-aria-min-chars'); + this.ariaOneResult = ariaOneResult || context.getAttribute('data-aria-one-result'); + this.ariaNResults = ariaNResults || context.getAttribute('data-aria-n-results'); + this.ariaLimitedResults = ariaLimitedResults || context.getAttribute('data-aria-limited-results'); + this.moreResults = moreResults || context.getAttribute('data-more-results'); + this.resultsTitle = resultsTitle || context.getAttribute('data-results-title'); + this.noResults = noResults || context.getAttribute('data-no-results'); + + this.listboxId = this.listbox.getAttribute('id'); + this.minChars = minChars || 3; + this.resultLimit = resultLimit || 10; + this.suggestOnBoot = suggestOnBoot; + this.lang = lang || 'en-gb'; // Callbacks this.onSelect = onSelect; this.onUnsetResult = onUnsetResult; this.onError = onError; - // Settings - this.content = JSON.parse(context.getAttribute('data-content')); - this.listboxId = this.listbox.getAttribute('id'); - this.minChars = minChars || 2; - this.resultLimit = resultLimit || null; - if (suggestionFunction) { this.fetchSuggestions = suggestionFunction; } @@ -72,21 +84,48 @@ export default class TypeaheadCore { this.resultSelected = false; this.blurring = false; this.blurTimeout = null; - this.sanitisedQueryReplaceChars = sanitisedQueryReplaceChars; - this.lang = document.documentElement.getAttribute('lang').toLowerCase(); + this.sanitisedQueryReplaceChars = sanitisedQueryReplaceChars || []; - // Modify DOM - this.label.setAttribute('for', this.input.getAttribute('id')); + // Temporary fix as runner doesn't use full lang code + if (this.lang === 'en') { + this.lang = 'en-gb'; + } + this.fetchData(); + this.initialiseUI(); + } + + initialiseUI() { this.input.setAttribute('aria-autocomplete', 'list'); this.input.setAttribute('aria-controls', this.listbox.getAttribute('id')); this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id')); - this.input.setAttribute('autocomplete', this.input.getAttribute('data-autocomplete')); - this.context.classList.add('typeahead--initialised'); + this.input.setAttribute('aria-has-popup', true); + this.input.setAttribute('aria-owns', this.listbox.getAttribute('id')); + this.input.setAttribute('aria-expanded', false); + this.input.setAttribute('role', 'combobox'); + + this.context.classList.add('typeahead-input--initialised'); - // Bind event listeners this.bindEventListeners(); } + fetchData() { + async function loadJSON(jsonPath) { + try { + const jsonResponse = await fetch(jsonPath); + if (jsonResponse.status === 500) { + throw new Error('Error fetching json data: ' + jsonResponse.status); + } + const jsonData = await jsonResponse.json(); + return jsonData; + } catch (error) { + console.log(error); + } + } + + // Call loading of json file + this.data = loadJSON(this.typeaheadData); + } + bindEventListeners() { this.input.addEventListener('keydown', this.handleKeydown.bind(this)); this.input.addEventListener('keyup', this.handleKeyup.bind(this)); @@ -99,21 +138,20 @@ export default class TypeaheadCore { } handleKeydown(event) { - this.ctrlKey = ((event.ctrlKey || event.metaKey) && event.keyCode !== KEYCODE.V); + this.ctrlKey = (event.ctrlKey || event.metaKey) && event.key !== 'v'; - switch (event.keyCode) { - case KEYCODE.UP: { + switch (event.key) { + case 'ArrowUp': { event.preventDefault(); this.navigateResults(-1); break; } - case KEYCODE.DOWN: { + case 'ArrowDown': { event.preventDefault(); this.navigateResults(1); break; } - case KEYCODE.ENTER: - case KEYCODE.RETURN: { + case 'Enter': { event.preventDefault(); break; } @@ -121,19 +159,18 @@ export default class TypeaheadCore { } handleKeyup(event) { - switch (event.keyCode) { - case KEYCODE.UP: - case KEYCODE.DOWN: { + switch (event.key) { + case 'ArrowUp': + case 'ArrowDown': { event.preventDefault(); break; } - case KEYCODE.ENTER: - case KEYCODE.RETURN: { - this.selectResult(); - break; - } - case KEYCODE.LEFT: - case KEYCODE.RIGHT: { + case 'Enter': { + if (this.highlightedResultIndex == null) { + this.clearListbox(); + } else { + this.selectResult(); + } break; } } @@ -142,14 +179,15 @@ export default class TypeaheadCore { } handleChange() { - if (!this.blurring) { + if (!this.blurring && this.input.value.trim()) { this.getSuggestions(); + } else { + this.abortFetch(); } } handleFocus() { clearTimeout(this.blurTimeout); - this.combobox.classList.add(classTypeaheadComboboxFocused); this.getSuggestions(true); } @@ -158,7 +196,6 @@ export default class TypeaheadCore { this.blurring = true; this.blurTimeout = setTimeout(() => { - this.combobox.classList.remove(classTypeaheadComboboxFocused); this.blurring = false; }, 300); } @@ -199,7 +236,6 @@ export default class TypeaheadCore { if (!this.settingResult) { const query = this.input.value; const sanitisedQuery = sanitiseTypeaheadText(query, this.sanitisedQueryReplaceChars); - if (sanitisedQuery !== this.sanitisedQuery || (force && !this.resultSelected)) { this.unsetResults(); this.setAriaStatus(); @@ -208,13 +244,15 @@ export default class TypeaheadCore { this.sanitisedQuery = sanitisedQuery; if (this.sanitisedQuery.length >= this.minChars) { - this.fetchSuggestions(this.sanitisedQuery) - .then(this.handleResults.bind(this)) - .catch(error => { - if (error.name !== 'AbortError' && this.onError) { - this.onError(error); - } - }); + this.data.then(data => { + this.fetchSuggestions(this.sanitisedQuery, data) + .then(this.handleResults.bind(this)) + .catch(error => { + if (error.name !== 'AbortError' && this.onError) { + this.onError(error); + } + }); + }); } else { this.clearListbox(); } @@ -222,57 +260,34 @@ export default class TypeaheadCore { } } - fetchSuggestions(sanitisedQuery) { - return new Promise((resolve, reject) => { - const query = { - query: sanitisedQuery, - lang: this.lang - }; - - if (this.fetch && this.fetch.status !== 'DONE') { - this.fetch.abort(); + async fetchSuggestions(sanitisedQuery, data) { + this.abortFetch(); + const results = await queryJson(sanitisedQuery, data, this.lang, this.resultLimit); + results.forEach(result => { + result.sanitisedText = sanitiseTypeaheadText(result[this.lang], this.sanitisedQueryReplaceChars); + if (this.lang !== 'en-gb') { + const english = result['en-gb']; + const sanitisedAlternative = sanitiseTypeaheadText(english, this.sanitisedQueryReplaceChars); + + if (sanitisedAlternative.match(sanitisedQuery)) { + result.alternatives = [english]; + result.sanitisedAlternatives = [sanitisedAlternative]; + } + } else { + result.alternatives = []; + result.sanitisedAlternatives = []; } - - const body = formBodyFromObject(query); - - this.fetch = new AbortableFetch(this.apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body - }); - - this.fetch.send() - .then(async response => { - const data = await response.json(); - - const results = data.results; - - results.forEach(result => { - result.sanitisedText = sanitiseTypeaheadText(result[this.lang], this.sanitisedQueryReplaceChars); - - if (this.lang !== 'en-gb') { - const english = result['en-gb']; - const sanitisedAlternative = sanitiseTypeaheadText(english, this.sanitisedQueryReplaceChars); - - if (sanitisedAlternative.match(sanitisedQuery)) { - result.alternatives = [english]; - result.sanitisedAlternatives = [sanitisedAlternative]; - } - } else { - result.alternatives = []; - result.sanitisedAlternatives = []; - } - }); - - resolve({ - results, - totalResults: data.totalResults - }); - }) - .catch(reject); }); + return { + results, + totalResults: results.length, + }; + } + + abortFetch() { + if (this.fetch && this.fetch.status !== 'DONE') { + this.fetch.abort(); + } } unsetResults() { @@ -289,7 +304,7 @@ export default class TypeaheadCore { this.listbox.innerHTML = ''; this.context.classList.remove(classTypeaheadHasResults); this.input.removeAttribute('aria-activedescendant'); - this.combobox.removeAttribute('aria-expanded'); + this.input.removeAttribute('aria-expanded'); if (!preventAriaStatusUpdate) { this.setAriaStatus(); @@ -298,11 +313,16 @@ export default class TypeaheadCore { handleResults(result) { this.foundResults = result.totalResults; + + if (result.results.length > 10) { + result.results = result.results.slice(0, this.resultLimit); + } + this.results = result.results; this.numberOfResults = Math.max(this.results.length, 0); if (!this.deleting || (this.numberOfResults && this.deleting)) { - if (this.numberOfResults.length === 1 && this.results[0].sanitisedText === this.sanitisedQuery) { + if (this.numberOfResults === 1 && this.results[0].sanitisedText === this.sanitisedQuery) { this.clearListbox(true); this.selectResult(0); } else { @@ -312,7 +332,9 @@ export default class TypeaheadCore { let innerHTML = this.emboldenMatch(ariaLabel, this.query); if (Array.isArray(result.sanitisedAlternatives)) { - const alternativeMatch = result.sanitisedAlternatives.find(alternative => alternative !== result.sanitisedText && alternative.includes(this.sanitisedQuery)); + const alternativeMatch = result.sanitisedAlternatives.find( + alternative => alternative !== result.sanitisedText && alternative.includes(this.sanitisedQuery), + ); if (alternativeMatch) { const alternativeText = result.alternatives[result.sanitisedAlternatives.indexOf(alternativeMatch)]; @@ -325,7 +347,6 @@ export default class TypeaheadCore { listElement.className = classTypeaheadOption; listElement.setAttribute('id', `${this.listboxId}__option--${index}`); listElement.setAttribute('role', 'option'); - listElement.setAttribute('tabindex', '-1'); listElement.setAttribute('aria-label', ariaLabel); listElement.innerHTML = innerHTML; @@ -340,30 +361,28 @@ export default class TypeaheadCore { if (this.numberOfResults < this.foundResults) { const listElement = document.createElement('li'); - listElement.className = `${classTypeaheadOption} ${classTypeaheadOptionMoreResults} u-fs-b`; - listElement.setAttribute('tabindex', '-1'); + listElement.className = `${classTypeaheadOption} ${classTypeaheadOptionMoreResults}`; listElement.setAttribute('aria-hidden', 'true'); - listElement.innerHTML = this.content.more_results; + listElement.innerHTML = this.moreResults; this.listbox.appendChild(listElement); } this.setHighlightedResult(null); - this.combobox.setAttribute('aria-expanded', true); - this.context.classList.add(classTypeaheadHasResults); + + this.input.setAttribute('aria-expanded', !!this.numberOfResults); + this.context.classList[!!this.numberOfResults ? 'add' : 'remove'](classTypeaheadHasResults); } } - - if (this.numberOfResults === 0 && this.content.no_results) { - this.listbox.innerHTML = `
  • ${this.content.no_results}
  • `; - this.combobox.setAttribute('aria-expanded', true); - this.context.classList.add(classTypeaheadHasResults); + if (this.numberOfResults === 0 && this.noResults) { + this.listbox.innerHTML = `
  • ${this.noResults}
  • `; + this.input.setAttribute('aria-expanded', true); } } setHighlightedResult(index) { this.highlightedResultIndex = index; - if (this.setHighlightedResult === null) { + if (this.highlightedResultIndex === null) { this.input.removeAttribute('aria-activedescendant'); } else if (this.numberOfResults) { this.resultOptions.forEach((option, optionIndex) => { @@ -387,16 +406,16 @@ export default class TypeaheadCore { const noResults = this.numberOfResults === 0; if (queryTooShort) { - content = this.content.aria_min_chars; + content = this.ariaMinChars; } else if (noResults) { - content = `${this.content.aria_no_results}: "${this.query}"`; + content = `${this.ariaNoResults}: "${this.query}"`; } else if (this.numberOfResults === 1) { - content = this.content.aria_one_result; + content = this.ariaOneResult; } else { - content = this.content.aria_n_results.replace('{n}', this.numberOfResults); + content = this.ariaNResults.replace('{n}', this.numberOfResults); if (this.resultLimit && this.foundResults > this.resultLimit) { - content += ` ${this.content.aria_limited_results}`; + content += ` ${this.ariaLimitedResults}`; } } } @@ -410,30 +429,30 @@ export default class TypeaheadCore { const result = this.results[index || this.highlightedResultIndex || 0]; - // TODO: This condition should be removed if we go with the internal address lookup API, or made configurable if we use a third party API - if (result.type !== 'Postcode') { - this.input.value = result[this.lang]; - this.query = result[this.lang]; - } - this.resultSelected = true; - this.onSelect(result).then(() => { - this.settingResult = false; - // this.input.setAttribute('autocomplete', 'false'); - }); - - let ariaAlternativeMessage = ''; + if (result.sanitisedText !== this.sanitisedQuery && result.sanitisedAlternatives && result.sanitisedAlternatives.length) { + const bestMatchingAlternative = result.sanitisedAlternatives + .map((alternative, index) => ({ + score: dice(this.sanitisedQuery, alternative), + index, + })) + .sort(sortBy('score'))[0]; - if (!result.sanitisedText.includes(this.sanitisedQuery) && result.sanitisedAlternatives) { - const alternativeMatch = result.sanitisedAlternatives.find(alternative => alternative.includes(this.sanitisedQuery)); + const scoredSanitised = dice(this.sanitisedQuery, result.sanitisedText); - if (alternativeMatch) { - ariaAlternativeMessage = `, ${this.content.aria_found_by_alternative_name}: ${alternativeMatch}`; + if (bestMatchingAlternative.score >= scoredSanitised) { + result.displayText = result.alternatives[bestMatchingAlternative.index]; + } else { + result.displayText = result[this.lang]; } + } else { + result.displayText = result[this.lang]; } - const ariaMessage = `${this.content.aria_you_have_selected}: ${result[this.lang]}${ariaAlternativeMessage}.`; + this.onSelect(result).then(() => (this.settingResult = false)); + + const ariaMessage = `${this.ariaYouHaveSelected}: ${result.displayText}.`; this.clearListbox(); this.setAriaStatus(ariaMessage); @@ -451,7 +470,7 @@ export default class TypeaheadCore { const match = string.substr(matchIndex, queryLength); const after = string.substr(matchEnd, string.length - matchEnd); - return `${before}${match}${after}`; + return `${before}${match}${after}`; } else { return string; } diff --git a/_prototypes/individual-response--your-household-v15/bundle.js b/_prototypes/individual-response--your-household-v15/bundle.js index ae90b076b4..a8b2c3d783 100644 --- a/_prototypes/individual-response--your-household-v15/bundle.js +++ b/_prototypes/individual-response--your-household-v15/bundle.js @@ -8,11 +8,12 @@ import './assets/lib/array-find-polyfill'; import './assets/lib/CustomEvent-polyfill'; import './assets/lib/fetch-polyfill'; import './assets/lib/abortcontroller-polyfill'; +import "regenerator-runtime/runtime"; /** * DOM modules */ -import './assets/modules/typeahead-refactored/typeahead.module'; +import './assets/modules/typeahead/typeahead'; import './assets/modules/uac/uac'; import { @@ -124,6 +125,7 @@ import { clearPersonalBookmark, personalQuestionSubmitDecorator } from './assets/personal-details'; + import { removeFromList, trailingNameS } from './assets/utils'; import { numberToPositionWord, numberToWordsStyleguide, precedingOrdinalWord } from './assets/numbers-to-words'; diff --git a/_prototypes/individual-response--your-household-v15/individual-details-country-other.html b/_prototypes/individual-response--your-household-v15/individual-details-country-other.html index fe9ca29810..0ecfb563bb 100644 --- a/_prototypes/individual-response--your-household-v15/individual-details-country-other.html +++ b/_prototypes/individual-response--your-household-v15/individual-details-country-other.html @@ -17,9 +17,6 @@
    -
    @@ -39,27 +36,27 @@
    -
    -
    -
    - - -
    +
    +
    +

    + + +

    + +
    +
    Suggestions
    +
      +
      +
      Use up and down keys to navigate suggestions once you've typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures.
      +
      +
      +

      +
      @@ -70,11 +67,6 @@

      Can't complete this question?
      Choose another section and return to this later

      - -
      diff --git a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-description.html b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-description.html index 85def16e80..22b3618461 100644 --- a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-description.html +++ b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-description.html @@ -16,15 +16,13 @@
      - +
      + action="../individual-details-religion-description"> Choose another section and return to this later

      -
      @@ -101,7 +95,7 @@ storageAPI.addUpdateEthnicGroupDescription(personId, val); - if(val === 'other' && ethnicGroup !== 'other') { + if(val === 'other') { e.preventDefault(); window.location.href = '../individual-details-ethnic-group-other/?person_id=' + personId + diff --git a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-other.html b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-other.html index cb20cac665..f06820d65f 100644 --- a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-other.html +++ b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group-other.html @@ -17,14 +17,11 @@
      -
      + role="form" autocomplete="new-password" action="../individual-details-religion-description">

      - How would you describe your White ethnic group or background? + How would you describe your ethnic group or background?

      -
      -
      -
      - - -
      +
      +
      +

      + + +

      + +
      +
      Suggestions
      +
        +
        +
        Use up and down keys to navigate suggestions once you've typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures.
        +
        +
        +

        +
        @@ -75,11 +67,6 @@

        Can't complete this question?
        Choose another section and return to this later

        - -
        @@ -98,32 +85,6 @@ details = storageAPI.getPersonalDetailsFor(personId), surveyType = urlParams.get('survey'); - ethnicGroupMap = { - 'white': { - 'question': 'Any other White background', - 'category': 'White ethnic group or background', - 'api-endpoint': 'ethnic-groups-white' - }, - 'mixed': { - 'question': 'Any other Mixed or Multiple background', - 'category': 'Mixed or Multiple', - 'api-endpoint': 'ethnic-groups-mixed' - }, - 'asian': { - 'question': 'Other Asian ethnic group or background', - 'category': 'Asian or Asian British', - 'api-endpoint': 'ethnic-groups-asian' - }, - 'black': { - 'question': 'Any other Black, African or Caribbean background', - 'category': 'Black, African, Caribbean or Black British', - 'api-endpoint': 'ethnic-groups-black' - }/*, - 'other': { - 'question': '' - }*/ - }; - $('.qa-btn-submit').on('click', storageAPI.personalQuestionSubmitDecorator.bind(null, personId, function (e) { var otherText = $('#ethnic-group-input').val(); @@ -144,27 +105,8 @@ $('#survey-type').val(surveyType); } - function updateQuestion() { - var str = 'You selected “' + ethnicGroupMap[ethnicGroup].question + '”. ' + - 'How would you describe ' + - (storageAPI.getAnsweringIndividualByProxy() - ? ('' + person.fullName + window.ONS.utils.trailingNameS(person.fullName) + '') - : 'your') + - ' ' + ethnicGroupMap[ethnicGroup].category + - '?'; - - $('.js-heading').html(str); - } - function updateAnswer() { - $('.js-typeahead-label').html(ethnicGroupMap[ethnicGroup].category + '
        Enter your own answer or select from suggestions
        '); - - $('.js-typeahead').attr('data-api-url', - 'https://ons-typeahead-prototypes.herokuapp.com/' + - ethnicGroupMap[ethnicGroup]['api-endpoint']); - document.dispatchEvent(new CustomEvent('TYPEAHEAD-READY')); - //data-api-url="https://ons-typeahead-prototypes.herokuapp.com/ethnic-groups-white" } if (details && details['ethnic-group'] && @@ -178,7 +120,6 @@ $(updateAllPreviousLinks); - $(updateQuestion); $(updateRouting); $(updateAnswer); diff --git a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group.html b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group.html index fddd1b2338..1cd7520e19 100644 --- a/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group.html +++ b/_prototypes/individual-response--your-household-v15/individual-details-ethnic-group.html @@ -54,7 +54,7 @@ id="white-label"> White

        - Include British, + Includes British, Northern Irish, Irish, Gypsy, Irish Traveller, Roma or diff --git a/_prototypes/individual-response--your-household-v15/individual-details-job-title.html b/_prototypes/individual-response--your-household-v15/individual-details-job-title.html index 20238f6bfc..f4507a4354 100644 --- a/_prototypes/individual-response--your-household-v15/individual-details-job-title.html +++ b/_prototypes/individual-response--your-household-v15/individual-details-job-title.html @@ -47,7 +47,7 @@