input-duplicator: migrate to TypeScript (#230)

This commit is contained in:
liamwhite 2024-04-22 18:43:27 -04:00 committed by GitHub
parent ac3b15b1e2
commit 88a1131f35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 167 additions and 83 deletions

View file

@ -0,0 +1,91 @@
import { inputDuplicatorCreator } from '../input-duplicator';
import { assertNotNull } from '../utils/assert';
import { $, $$, removeEl } from '../utils/dom';
describe('Input duplicator functionality', () => {
beforeEach(() => {
document.documentElement.insertAdjacentHTML('beforeend', `<form action="/">
<div class="js-max-input-count">3</div>
<div class="js-input-source">
<input id="0" name="0" class="js-input" type="text"/>
<label>
<a href="#" class="js-remove-input">Delete</a>
</label>
</div>
<div class="js-button-container">
<button type="button" class="js-add-input">Add input</button>
</div>
</form>`);
});
afterEach(() => {
removeEl($$<HTMLFormElement>('form'));
});
function runCreator() {
inputDuplicatorCreator({
addButtonSelector: '.js-add-input',
fieldSelector: '.js-input-source',
maxInputCountSelector: '.js-max-input-count',
removeButtonSelector: '.js-remove-input',
});
}
it('should ignore forms without a duplicator button', () => {
removeEl($$<HTMLButtonElement>('button'));
expect(runCreator()).toBeUndefined();
});
it('should duplicate the input elements', () => {
runCreator();
expect($$('input')).toHaveLength(1);
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
expect($$('input')).toHaveLength(2);
});
it('should duplicate the input elements when the button is before the inputs', () => {
const form = assertNotNull($<HTMLFormElement>('form'));
const buttonDiv = assertNotNull($<HTMLDivElement>('.js-button-container'));
removeEl(buttonDiv);
form.insertAdjacentElement('afterbegin', buttonDiv);
runCreator();
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
expect($$('input')).toHaveLength(2);
});
it('should not create more input elements than the limit', () => {
runCreator();
for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
}
expect($$('input')).toHaveLength(3);
});
it('should remove duplicated input elements', () => {
runCreator();
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
expect($$('input')).toHaveLength(1);
});
it('should not remove the last input element', () => {
runCreator();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
}
expect($$('input')).toHaveLength(1);
});
});

View file

@ -1,83 +0,0 @@
import { $, $$, disableEl, enableEl, removeEl } from './utils/dom';
import { delegate, leftClick } from './utils/events';
/**
* @typedef InputDuplicatorOptions
* @property {string} addButtonSelector
* @property {string} fieldSelector
* @property {string} maxInputCountSelector
* @property {string} removeButtonSelector
*/
/**
* @param {InputDuplicatorOptions} options
*/
function inputDuplicatorCreator({
addButtonSelector,
fieldSelector,
maxInputCountSelector,
removeButtonSelector
}) {
const addButton = $(addButtonSelector);
if (!addButton) {
return;
}
const form = addButton.closest('form');
const fieldRemover = (event, target) => {
event.preventDefault();
// Prevent removing the final field element to not "brick" the form
const existingFields = $$(fieldSelector, form);
if (existingFields.length <= 1) {
return;
}
removeEl(target.closest(fieldSelector));
enableEl(addButton);
};
delegate(document, 'click', {
[removeButtonSelector]: leftClick(fieldRemover)
});
const maxOptionCount = parseInt($(maxInputCountSelector, form).innerHTML, 10);
addButton.addEventListener('click', e => {
e.preventDefault();
const existingFields = $$(fieldSelector, form);
let existingFieldsLength = existingFields.length;
if (existingFieldsLength < maxOptionCount) {
// The last element matched by the `fieldSelector` will be the last field, make a copy
const prevField = existingFields[existingFieldsLength - 1];
const prevFieldCopy = prevField.cloneNode(true);
const prevFieldCopyInputs = $$('input', prevFieldCopy);
prevFieldCopyInputs.forEach(prevFieldCopyInput => {
// Reset new input's value
prevFieldCopyInput.value = '';
prevFieldCopyInput.removeAttribute('value');
// Increment sequential attributes of the input
['name', 'id'].forEach(attr => {
prevFieldCopyInput.setAttribute(attr, prevFieldCopyInput[attr].replace(/\d+/g, `${existingFieldsLength}`));
});
});
// Insert copy before the last field's next sibling, or if none, at the end of its parent
if (prevField.nextElementSibling) {
prevField.parentNode.insertBefore(prevFieldCopy, prevField.nextElementSibling);
}
else {
prevField.parentNode.appendChild(prevFieldCopy);
}
existingFieldsLength++;
}
// Remove the button if we reached the max number of options
if (existingFieldsLength >= maxOptionCount) {
disableEl(addButton);
}
});
}
export { inputDuplicatorCreator };

View file

@ -0,0 +1,76 @@
import { assertNotNull } from './utils/assert';
import { $, $$, disableEl, enableEl, removeEl } from './utils/dom';
import { delegate, leftClick } from './utils/events';
export interface InputDuplicatorOptions {
addButtonSelector: string;
fieldSelector: string;
maxInputCountSelector: string;
removeButtonSelector: string;
}
export function inputDuplicatorCreator({
addButtonSelector,
fieldSelector,
maxInputCountSelector,
removeButtonSelector
}: InputDuplicatorOptions) {
const addButton = $<HTMLButtonElement>(addButtonSelector);
if (!addButton) {
return;
}
const form = assertNotNull(addButton.closest('form'));
const fieldRemover = (event: MouseEvent, target: HTMLElement) => {
event.preventDefault();
// Prevent removing the final field element to not "brick" the form
const existingFields = $$(fieldSelector, form);
if (existingFields.length <= 1) {
return;
}
removeEl(assertNotNull(target.closest<HTMLElement>(fieldSelector)));
enableEl(addButton);
};
delegate(form, 'click', {
[removeButtonSelector]: leftClick(fieldRemover)
});
const maxOptionCountElement = assertNotNull($(maxInputCountSelector, form));
const maxOptionCount = parseInt(maxOptionCountElement.innerHTML, 10);
addButton.addEventListener('click', e => {
e.preventDefault();
const existingFields = $$<HTMLElement>(fieldSelector, form);
let existingFieldsLength = existingFields.length;
if (existingFieldsLength < maxOptionCount) {
// The last element matched by the `fieldSelector` will be the last field, make a copy
const prevField = existingFields[existingFieldsLength - 1];
const prevFieldCopy = prevField.cloneNode(true) as HTMLElement;
$$<HTMLInputElement>('input', prevFieldCopy).forEach(prevFieldCopyInput => {
// Reset new input's value
prevFieldCopyInput.value = '';
prevFieldCopyInput.removeAttribute('value');
// Increment sequential attributes of the input
prevFieldCopyInput.setAttribute('name', prevFieldCopyInput.name.replace(/\d+/g, `${existingFieldsLength}`));
prevFieldCopyInput.setAttribute('id', prevFieldCopyInput.id.replace(/\d+/g, `${existingFieldsLength}`));
});
prevField.insertAdjacentElement('afterend', prevFieldCopy);
existingFieldsLength++;
}
// Remove the button if we reached the max number of options
if (existingFieldsLength >= maxOptionCount) {
disableEl(addButton);
}
});
}