diff --git a/assets/js/__tests__/search.spec.ts b/assets/js/__tests__/search.spec.ts new file mode 100644 index 00000000..abc033b5 --- /dev/null +++ b/assets/js/__tests__/search.spec.ts @@ -0,0 +1,99 @@ +import { $ } from '../utils/dom'; +import { assertNotNull } from '../utils/assert'; +import { setupSearch } from '../search'; +import { setupTagListener } from '../tagsinput'; + +const formData = `
+ + NOT + Numeric ID + My favorites + + +
`; + +describe('Search form help', () => { + beforeAll(() => { + setupSearch(); + setupTagListener(); + }); + + let input: HTMLInputElement; + let prependAnchor: HTMLAnchorElement; + let idAnchor: HTMLAnchorElement; + let favesAnchor: HTMLAnchorElement; + let helpNumeric: HTMLDivElement; + let subjectSpan: HTMLElement; + + beforeEach(() => { + document.body.innerHTML = formData; + + input = assertNotNull($('input')); + prependAnchor = assertNotNull($('a[data-search-prepend]')); + idAnchor = assertNotNull($('a[data-search-add="id.lte:10"]')); + favesAnchor = assertNotNull($('a[data-search-add="my:faves"]')); + helpNumeric = assertNotNull($('[data-search-help="numeric"]')); + subjectSpan = assertNotNull($('span', helpNumeric)); + }); + + it('should add text to input field', () => { + idAnchor.click(); + expect(input.value).toBe('id.lte:10'); + + favesAnchor.click(); + expect(input.value).toBe('id.lte:10, my:faves'); + }); + + it('should focus and select text in input field when requested', () => { + idAnchor.click(); + expect(input).toHaveFocus(); + expect(input.selectionStart).toBe(7); + expect(input.selectionEnd).toBe(9); + }); + + it('should highlight subject name when requested', () => { + expect(helpNumeric).toHaveClass('hidden'); + idAnchor.click(); + expect(helpNumeric).not.toHaveClass('hidden'); + expect(subjectSpan).toHaveTextContent('Numeric ID'); + }); + + it('should not focus and select text in input field when unavailable', () => { + favesAnchor.click(); + expect(input).not.toHaveFocus(); + expect(input.selectionStart).toBe(8); + expect(input.selectionEnd).toBe(8); + }); + + it('should not highlight subject name when unavailable', () => { + favesAnchor.click(); + expect(helpNumeric).toHaveClass('hidden'); + }); + + it('should prepend to empty input', () => { + prependAnchor.click(); + expect(input.value).toBe('-'); + }); + + it('should prepend to single input', () => { + input.value = 'a'; + prependAnchor.click(); + expect(input.value).toBe('-a'); + }); + + it('should prepend to comma-separated input', () => { + input.value = 'a,b'; + prependAnchor.click(); + expect(input.value).toBe('a,-b'); + }); + + it('should prepend to comma and space-separated input', () => { + input.value = 'a, b'; + prependAnchor.click(); + expect(input.value).toBe('a, -b'); + }); +}); diff --git a/assets/js/search.js b/assets/js/search.js deleted file mode 100644 index 50733fd9..00000000 --- a/assets/js/search.js +++ /dev/null @@ -1,45 +0,0 @@ -import { $, $$ } from './utils/dom'; -import { addTag } from './tagsinput'; - -function showHelp(subject, type) { - $$('[data-search-help]').forEach(helpBox => { - if (helpBox.getAttribute('data-search-help') === type) { - $('.js-search-help-subject', helpBox).textContent = subject; - helpBox.classList.remove('hidden'); - } else { - helpBox.classList.add('hidden'); - } - }); -} - -function prependToLast(field, value) { - const separatorIndex = field.value.lastIndexOf(','); - const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1; - field.value = - field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy); -} - -function selectLast(field, characterCount) { - field.focus(); - - field.selectionStart = field.value.length - characterCount; - field.selectionEnd = field.value.length; -} - -function executeFormHelper(e) { - const searchField = $('.js-search-field'); - const attr = name => e.target.getAttribute(name); - - attr('data-search-add') && addTag(searchField, attr('data-search-add')); - attr('data-search-show-help') && showHelp(e.target.textContent, attr('data-search-show-help')); - attr('data-search-select-last') && selectLast(searchField, parseInt(attr('data-search-select-last'), 10)); - attr('data-search-prepend') && prependToLast(searchField, attr('data-search-prepend')); -} - -function setupSearch() { - const form = $('.js-search-form'); - - form && form.addEventListener('click', executeFormHelper); -} - -export { setupSearch }; diff --git a/assets/js/search.ts b/assets/js/search.ts new file mode 100644 index 00000000..eff8d98a --- /dev/null +++ b/assets/js/search.ts @@ -0,0 +1,85 @@ +import { assertNotNull, assertNotUndefined } from './utils/assert'; +import { $, $$, showEl, hideEl } from './utils/dom'; +import { delegate, leftClick } from './utils/events'; +import { addTag } from './tagsinput'; + +function focusAndSelectLast(field: HTMLInputElement, characterCount: number) { + field.focus(); + field.selectionStart = field.value.length - characterCount; + field.selectionEnd = field.value.length; +} + +function prependToLast(field: HTMLInputElement, value: string) { + // Find the last comma in the input and advance past it + const separatorIndex = field.value.lastIndexOf(','); + const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1; + + // Insert the value string at the new location + field.value = [ + field.value.slice(0, separatorIndex + advanceBy), + value, + field.value.slice(separatorIndex + advanceBy), + ].join(''); +} + +function getAssociatedData(target: HTMLElement) { + const form = assertNotNull(target.closest('form')); + const input = assertNotNull($('.js-search-field', form)); + const helpBoxes = $$('[data-search-help]', form); + + return { input, helpBoxes }; +} + +function showHelp(helpBoxes: HTMLDivElement[], typeName: string, subject: string) { + for (const helpBox of helpBoxes) { + // Get the subject name span + const subjectName = assertNotNull($('.js-search-help-subject', helpBox)); + + // Take the appropriate action for this help box + if (helpBox.dataset.searchHelp === typeName) { + subjectName.textContent = subject; + showEl(helpBox); + } else { + hideEl(helpBox); + } + } +} + +function onSearchAdd(_event: Event, target: HTMLAnchorElement) { + // Load form + const { input, helpBoxes } = getAssociatedData(target); + + // Get data for this link + const addValue = assertNotUndefined(target.dataset.searchAdd); + const showHelpValue = assertNotUndefined(target.dataset.searchShowHelp); + const selectLastValue = target.dataset.searchSelectLast; + + // Add the tag + addTag(input, addValue); + + // Show associated help, if available + showHelp(helpBoxes, showHelpValue, assertNotNull(target.textContent)); + + // Select last characters, if requested + if (selectLastValue) { + focusAndSelectLast(input, Number(selectLastValue)); + } +} + +function onSearchPrepend(_event: Event, target: HTMLAnchorElement) { + // Load form + const { input } = getAssociatedData(target); + + // Get data for this link + const prependValue = assertNotUndefined(target.dataset.searchPrepend); + + // Prepend + prependToLast(input, prependValue); +} + +export function setupSearch() { + delegate(document, 'click', { + 'form.js-search-form a[data-search-add][data-search-show-help]': leftClick(onSearchAdd), + 'form.js-search-form a[data-search-prepend]': leftClick(onSearchPrepend), + }); +}