mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
input-duplicator: migrate to TypeScript (#230)
This commit is contained in:
parent
ac3b15b1e2
commit
88a1131f35
3 changed files with 167 additions and 83 deletions
91
assets/js/__tests__/input-duplicator.spec.ts
Normal file
91
assets/js/__tests__/input-duplicator.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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 };
|
76
assets/js/input-duplicator.ts
Normal file
76
assets/js/input-duplicator.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue