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