function phac_aspc_autocomplete_trigger_change(container_id) { setTimeout(() => { const container = document.getElementById(container_id); const el = container.querySelector('.textinput'); el.dispatchEvent(new Event('change', { bubbles: true })); }, 0) } function phac_aspc_autocomplete_clear_focus(container, activate_ring) { const hasFocus = container.querySelectorAll('.hasFocus'); for (const el of hasFocus) { el.classList.remove('hasFocus'); } const el = container.querySelector('.textinput'); el.removeAttribute('aria-activedescendant'); if (activate_ring) { container.closest('.phac_aspc_form_autocomplete_focus_ring') .classList.add('active'); container.querySelector('.textinput').focus(); } else { container.closest('.phac_aspc_form_autocomplete_focus_ring') .classList.remove('active'); } } function phac_aspc_autocomplete_hide_results(container) { const results = container.querySelector('.results'); const el = container.querySelector('.textinput'); el.setAttribute('aria-expanded', false); results.classList.remove('show'); } phac_aspc_autocomplete_blur_skip = {} function phac_aspc_autocomplete_blur_handler(event, name, sync = false, item = false) { // Handler responsible for blur events // Will remove the results when focus is no longer on the component, and update // the box value when multiselect is false requestAnimationFrame(function () { const parent = document.getElementById(`${name}__container`); const id = parent.getAttribute('id'); if (phac_aspc_autocomplete_blur_skip[id]) return false; if (!parent.contains(document.activeElement)) { // Focus has left the component // Reset the component's state phac_aspc_autocomplete_closed[id] = false; if (phac_aspc_autocomplete_keyup_debounce[id]) { clearTimeout(phac_aspc_autocomplete_keyup_debounce[id]) phac_aspc_autocomplete_keyup_debounce[id] = false; } // Get reference to box const el = document.getElementById(name + '__textinput'); // Abort active HTMX operations on the input box to avoid race conditions htmx.trigger(el, 'htmx:abort'); // Set the text input value const data_el = document.getElementById(name + '__data'); if (!sync) { el.value = ''; } else { el.value = data_el.getAttribute('data-phac-aspc-autocomplete'); } phac_aspc_autocomplete_set_initial_value(parent, true); // Get reference to list of results const results = document.getElementById(name + '__items'); // Get reference to aria live area const live = document.getElementById(name + '__info'); // Test if HTMX is currently in the process of swapping if (results.classList.contains('htmx-swapping')) { // To ensure the results are hidden, wait for HTMX to finish, then hide. results.addEventListener( 'htmx:afterSettle', () => { phac_aspc_autocomplete_hide_results(parent); } ); } // Hide the results phac_aspc_autocomplete_hide_results(parent); // Clear the live info live.innerHTML = ''; // Change the min-width of the text input back to the (small) default parent.querySelector('.textinput') .parentElement.classList.remove('ac-active'); // Ensure no elements remain 'focused', and set focus to input phac_aspc_autocomplete_clear_focus(parent, item); } }); } function phac_aspc_autocomplete_item_click_handler(event) { const container = event.target.closest('.phac_aspc_form_autocomplete'); const results = container.querySelector('.results'); const open = results && results.classList.contains('show'); if (open) { phac_aspc_autocomplete_clear_focus(container, true); phac_aspc_autocomplete_hide_results(container); } return true; } function phac_aspc_autocomplete_focus_handler(event) { const container = event.target.closest('.phac_aspc_form_autocomplete'); phac_aspc_autocomplete_clear_focus(container, true); phac_aspc_autocomplete_set_initial_value(container); setTimeout(() => { // Announce selected items to screen readers. (if any) const info = container.querySelector('.live_info'); info.innerHTML += ' '; }, 100); } const phac_aspc_autocomplete_initial_value = {}; function phac_aspc_autocomplete_set_initial_value(container, reset = false) { const id = container.getAttribute('id'); const el = container.querySelector('.textinput'); if (reset) { phac_aspc_autocomplete_initial_value[id] = undefined; return; } if (phac_aspc_autocomplete_initial_value[id] === undefined) { phac_aspc_autocomplete_initial_value[id] = el.value; } } phac_aspc_autocomplete_closed = {}; function phac_aspc_autocomplete_click_handler(event) { if (event.target.classList.contains('item')) return true; const container = event.target.closest('.phac_aspc_form_autocomplete'); const id = container.getAttribute('id'); const results = container.querySelector('.results'); const open = results && results.classList.contains('show'); const text_box = container.querySelector('.textinput'); phac_aspc_autocomplete_set_initial_value(container); phac_aspc_autocomplete_clear_focus(container, true); phac_aspc_autocomplete_closed[id] = open; if (open) { phac_aspc_autocomplete_hide_results(container); } else { text_box.dispatchEvent(new Event('phac_aspc_autocomplete_trigger')); } return false; } const phac_aspc_autocomplete_keyup_debounce = {}; function phac_aspc_autocomplete_keyup_handler(event) { if (event.keyCode === 13) return false; const debounce = phac_aspc_autocomplete_keyup_debounce; const value = phac_aspc_autocomplete_initial_value; const elem = event.target; const container = elem.closest('.phac_aspc_form_autocomplete'); const id = container.getAttribute('id'); phac_aspc_autocomplete_set_initial_value(container); if (debounce[id]) { clearTimeout(debounce[id]); debounce[id] = false; } const v = elem.value; debounce[id] = setTimeout(() => { if (!phac_aspc_autocomplete_closed[id] && v != value[id]) { elem.dispatchEvent(new Event('phac_aspc_autocomplete_trigger')); } else if ( phac_aspc_autocomplete_closed[id] && v != value[id] && v == '' ) { phac_aspc_autocomplete_closed[id] = false; } value[id] = v; }, 250); return true; } const phac_aspc_autocomplete_keydown_debounce = {}; function phac_aspc_autocomplete_keydown_handler(event) { if (event.target.classList.contains('textinput') && event.keyCode > 47) { // Expands the min-width of text input to a reasonable size when typing event.target.parentElement.classList.add('ac-active'); } else if (event.target.classList.contains('textinput') && event.keyCode === 8 && event.target.value.length === 1) { // Shrinks the min-width of text input back to the (small) default if // the text input is empty due to backspacing event.target.parentElement.classList.remove('ac-active'); } // Handler responsible for keyboard navigation (up, down, esc and backspace) const debounce = phac_aspc_autocomplete_keydown_debounce; const whereTo = (container, down = true, skip_element = true, count = 1) => { // This function determines which element should receive focus // TODO: bug with down if (!container) return null; const results = container.querySelector('.results'); let element = container.querySelector('.hasFocus'); const must_skip = Boolean(element); const fallback = down ? results.querySelector('a:first-child') : results.querySelector('a:last-child'); if (!element) element = fallback; if (!element) return null; const dir = down ? elem => elem.nextElementSibling : elem => elem.previousElementSibling; let el = skip_element && must_skip ? dir(element) : element; let counter = count; while (el && counter > 0) { if (el.getAttribute('href')) { if (counter === 1) return el; } if (counter !== 1) counter -= 1; el = dir(el); } if (counter > 0) return fallback; return null; } const switchFocus = (element, container) => { phac_aspc_autocomplete_clear_focus(container); const el = container.querySelector('.textinput'); el.setAttribute('aria-activedescendant', element.getAttribute('id')); element.classList.add('hasFocus'); element.scrollIntoView({ block: 'nearest' }) } const selectFocusedItem = (container) => { const item = container.querySelector('.hasFocus'); if (item) { item.dispatchEvent(new Event('click')); } return item; } const focusWhenResultsShown = (container, timeout, up) => { // This function uses polling to wait for the results to be shown before // moving focus. const id = container.getAttribute('id'); const results = container.querySelector('.results'); if (!results || !results.classList.contains('show')) { if (timeout > 0) { if (debounce[id]) clearTimeout(debounce[id]); debounce[id] = setTimeout( () => focusWhenResultsShown(container, timeout - 100, up), 100 ); } return false; } debounce[id] = undefined; phac_aspc_autocomplete_closed[id] = false; if (up) { const prev = whereTo(container, false); if (prev) switchFocus(prev, container); } else { const next = whereTo(container, true, false); if (next) switchFocus(next, container); } } const getPageSize = (container) => { const r1 = container.getBoundingClientRect(); const r2 = container.querySelector('.item').getBoundingClientRect(); return Math.floor((r1.bottom - r1.top) / (r2.bottom - r2.top)); } const container = event.target.closest('.phac_aspc_form_autocomplete'); const results = container.querySelector('.results'); const id = container.getAttribute('id'); phac_aspc_autocomplete_set_initial_value(container); if (event.keyCode === 27) { // Escape key if (results && results.classList.contains('show')) { phac_aspc_autocomplete_clear_focus(container, true); phac_aspc_autocomplete_hide_results(container); phac_aspc_autocomplete_closed[id] = true; } else { event.target.value = ''; } } else if (event.keyCode === 13) { // Enter key if (results && results.classList.contains('show')) { selectFocusedItem(container); phac_aspc_autocomplete_clear_focus(container, true); phac_aspc_autocomplete_hide_results(container); } return false; } else if ( event.keyCode === 8 && event.target.value.length === 0 ) { // Backspace key on text input const chip = container.querySelectorAll('.chip a'); if (chip.length > 0) chip[chip.length - 1].dispatchEvent(new Event('click')); } else if (event.keyCode === 33) { // Page up key if (results) { const prev = whereTo( container, false, true, getPageSize(results) ); if (prev) switchFocus(prev, container); return false; } } else if (event.keyCode === 34) { // Page down key if (results) { const next = whereTo( container, true, true, getPageSize(results) ); if (next) switchFocus(next, container); return false; } } else if (event.keyCode === 40) { // down arrow // Open the results if they are not shown if (!results || !results.classList.contains('show')) { event.target.dispatchEvent(new Event('phac_aspc_autocomplete_trigger')); if (event.altKey) { phac_aspc_autocomplete_closed[id] = false; } else { focusWhenResultsShown(container, 3000); } } else { const next = whereTo(container); if (next) switchFocus(next, container); } return false; } else if (event.keyCode === 38) { // up arrow on item // Open the results if they are not shown if (!results || !results.classList.contains('show')) { event.target.dispatchEvent(new Event('phac_aspc_autocomplete_trigger')); if (event.altKey) { phac_aspc_autocomplete_closed[id] = false; } else { focusWhenResultsShown(container, 3000, true); } } else { const prev = whereTo(container, false); if (prev) switchFocus(prev, container); } return false; } else { phac_aspc_autocomplete_closed[id] = false; } phac_aspc_autocomplete_clear_focus(container, true); return true; } class AbstractAutocompleteHelper { /* this is a helper class to manipulate autocomplete components creating instances has zero side-effects it's assumed you may instantiate the same component multiple times */ constructor(fieldName, componentPrefix="") { this.fieldName = fieldName; this.componentPrefix = componentPrefix; } getComponentId(){ return this.componentPrefix + this.fieldName; } getContainer(){ return document.getElementById(`${this.getComponentId()}__container`); } getInput(){ return this.getContainer().querySelector(`#${this.getComponentId()}__textinput`); } getInputWrapper(){ return this.getContainer().querySelector(`#${this.getComponentId()}`); } getResultItems(){ return this.getContainer().querySelector(`#${this.getComponentId()}__items`); } getInfo(){ return this.getContainer().querySelector(`#${this.getComponentId()}__info`); } getDataContainer(){ return this.getContainer().querySelector(`#${this.getComponentId()}__data`); } // behavioral methods clear(){ this.getInput().value = ''; this.getInputWrapper().innerHTML = ''; this.getResultItems().innerHTML = ''; this.getInfo().innerHTML = ''; this.getDataContainer().removeAttribute('data-phac-aspc-autocomplete'); } } class SingleAutocompleteHelper extends AbstractAutocompleteHelper {} class MultiAutocompleteHelper extends AbstractAutocompleteHelper { getSrDescription(){ return this.getContainer().querySelector(`#${this.getComponentId()}__sr_description`); } getChips(){ return this.getContainer().querySelectorAll(`#${this.getComponentId()}_ac_container li.chip`); } clear(){ super.clear(); this.getChips().forEach(chip => chip.remove()); this.getSrDescription().innerHTML = ''; } }