Merge pull request #366 from philomena-dev/search-ts

Convert search help box to TypeScript
This commit is contained in:
liamwhite 2024-11-10 09:51:20 -05:00 committed by GitHub
commit d749907d0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 184 additions and 45 deletions

View file

@ -0,0 +1,99 @@
import { $ } from '../utils/dom';
import { assertNotNull } from '../utils/assert';
import { setupSearch } from '../search';
import { setupTagListener } from '../tagsinput';
const formData = `<form class="js-search-form">
<input type="text" class="js-search-field">
<a data-search-prepend="-">NOT</a>
<a data-search-add="id.lte:10" data-search-select-last="2" data-search-show-help="numeric">Numeric ID</a>
<a data-search-add="my:faves" data-search-show-help=" ">My favorites</a>
<div class="hidden" data-search-help="boolean">
<span class="js-search-help-subject"></span> is a Boolean value field
</div>
<div class="hidden" data-search-help="numeric">
<span class="js-search-help-subject"></span> is a numerical range field
</div>
</form>`;
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($<HTMLInputElement>('input'));
prependAnchor = assertNotNull($<HTMLAnchorElement>('a[data-search-prepend]'));
idAnchor = assertNotNull($<HTMLAnchorElement>('a[data-search-add="id.lte:10"]'));
favesAnchor = assertNotNull($<HTMLAnchorElement>('a[data-search-add="my:faves"]'));
helpNumeric = assertNotNull($<HTMLDivElement>('[data-search-help="numeric"]'));
subjectSpan = assertNotNull($<HTMLSpanElement>('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');
});
});

View file

@ -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 };

85
assets/js/search.ts Normal file
View file

@ -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($<HTMLInputElement>('.js-search-field', form));
const helpBoxes = $$<HTMLDivElement>('[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($<HTMLElement>('.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),
});
}