Merge pull request #984 from victiondev/feat/allowHTML

feat: Introduce allowHTML option to allow people to disable injecting HTML into choices.
This commit is contained in:
Matt Triff 2021-12-26 09:47:07 -05:00 committed by GitHub
commit 0b6973b322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 872 additions and 325 deletions

View File

@ -109,6 +109,7 @@ Or include Choices directly:
removeItems: true,
removeItemButton: false,
editItems: false,
allowHTML: true
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,
@ -314,6 +315,16 @@ Pass an array of objects:
**Usage:** Whether a user can edit items. An item's value can be edited by pressing the backspace.
### allowHTML
**Type:** `Boolean` **Default:** `true`
**Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** Whether HTML should be rendered in all Choices elements. If `false`, all elements (placeholder, items, etc.) will be treated as plain text. If `true`, this can be used to perform XSS scripting attacks if you load choices from a remote source.
**Deprecation Warning:** This will default to `false` in a future release.
### duplicateItemsAllowed
**Type:** `Boolean` **Default:** `true`
@ -637,6 +648,8 @@ classNames: {
If you want just extend a little original template then you may use `Choices.defaults.templates` to get access to
original template function.
Templates receive the full Choices config as the first argument to any template, which allows you to conditionally display things based on the options specified.
**Example:**
```js
@ -656,7 +669,7 @@ or more complex:
const example = new Choices(element, {
callbackOnCreateTemplates: function(template) {
return {
item: (classNames, data) => {
item: ({ classNames }, data) => {
return template(`
<div class="${classNames.item} ${
data.highlighted
@ -671,7 +684,7 @@ const example = new Choices(element, {
</div>
`);
},
choice: (classNames, data) => {
choice: ({ classNames }, data) => {
return template(`
<div class="${classNames.item} ${classNames.itemChoice} ${
data.disabled ? classNames.itemDisabled : classNames.itemSelectable

View File

@ -1,6 +1,10 @@
describe('Choices - select multiple', () => {
beforeEach(() => {
cy.visit('/select-multiple');
cy.visit('/select-multiple', {
onBeforeLoad(win) {
cy.stub(win.console, 'warn').as('consoleWarn');
},
});
});
describe('scenarios', () => {
@ -865,5 +869,78 @@ describe('Choices - select multiple', () => {
});
});
});
describe('allow html', () => {
describe('is undefined', () => {
it('logs a deprecation warning', () => {
cy.get('@consoleWarn').should(
'be.calledOnceWithExactly',
'Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.',
);
});
it('does not show as text when selected', () => {
cy.get('[data-test-hook=allowhtml-undefined]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
it('does not show html as text in dropdown', () => {
cy.get('[data-test-hook=allowhtml-undefined]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 2');
});
});
});
describe('set to true', () => {
it('does not show as text when selected', () => {
cy.get('[data-test-hook=allowhtml-true]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
it('does not show html as text in dropdown', () => {
cy.get('[data-test-hook=allowhtml-true]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 2');
});
});
});
describe('set to false', () => {
it('shows html as text when selected', () => {
cy.get('[data-test-hook=allowhtml-false]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('<b>Choice 1</b>');
});
});
it('shows html as text', () => {
cy.get('[data-test-hook=allowhtml-false]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('<b>Choice 2</b>');
});
});
});
});
});
});

View File

@ -1,6 +1,10 @@
describe('Choices - select one', () => {
beforeEach(() => {
cy.visit('/select-one');
cy.visit('/select-one', {
onBeforeLoad(win) {
cy.stub(win.console, 'warn').as('consoleWarn');
},
});
});
describe('scenarios', () => {
@ -51,9 +55,7 @@ describe('Choices - select one', () => {
describe('selecting choices', () => {
beforeEach(() => {
// open dropdown
cy.get('[data-test-hook=basic]')
.find('.choices')
.click();
cy.get('[data-test-hook=basic]').find('.choices').click();
});
const selectedChoiceText = 'Choice 1';
@ -68,7 +70,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain(selectedChoiceText);
});
});
@ -84,7 +86,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($item => {
.should(($item) => {
expect($item).to.contain(selectedChoiceText);
});
});
@ -93,9 +95,7 @@ describe('Choices - select one', () => {
describe('searching choices', () => {
beforeEach(() => {
// open dropdown
cy.get('[data-test-hook=basic]')
.find('.choices')
.click();
cy.get('[data-test-hook=basic]').find('.choices').click();
});
describe('on input', () => {
@ -109,7 +109,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 2');
});
});
@ -125,7 +125,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 3');
});
});
@ -140,7 +140,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal('No results found');
});
@ -206,7 +206,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__list--single .choices__item')
.last()
.then($choice => {
.then(($choice) => {
removedChoiceText = $choice.text().trim();
})
.click();
@ -229,7 +229,7 @@ describe('Choices - select one', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.not.contain(removedChoiceText);
@ -248,7 +248,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=disabled-choice]')
.find('.choices__list--dropdown .choices__item--disabled')
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -258,7 +258,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--single .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text()).to.not.contain(selectedChoiceText);
});
});
@ -305,9 +305,7 @@ describe('Choices - select one', () => {
describe('on click', () => {
it('does not open choice dropdown', () => {
cy.get('[data-test-hook=disabled-via-attr]')
.find('.choices')
.click();
cy.get('[data-test-hook=disabled-via-attr]').find('.choices').click();
cy.get('[data-test-hook=disabled-via-attr]')
.find('.choices__list--dropdown')
@ -335,7 +333,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.last()
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -345,7 +343,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--single .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.data('value')).to.equal(
`before-${selectedChoiceText}-after`,
);
@ -356,7 +354,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--single .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text()).to.not.contain(
`before-${selectedChoiceText}-after`,
);
@ -389,9 +387,7 @@ describe('Choices - select one', () => {
const selectedChoiceText = 'Choice 3';
beforeEach(() => {
cy.get('[data-test-hook=search-disabled]')
.find('.choices')
.click();
cy.get('[data-test-hook=search-disabled]').find('.choices').click();
});
it('does not display a search input', () => {
@ -410,7 +406,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=search-disabled]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain(selectedChoiceText);
});
});
@ -442,7 +438,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.not.contain(searchTerm);
});
});
@ -460,7 +456,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.contain(searchTerm);
});
});
@ -476,7 +472,7 @@ describe('Choices - select one', () => {
.children()
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
.and(($placeholder) => {
expect($placeholder).to.contain('I am a placeholder');
});
});
@ -524,7 +520,7 @@ describe('Choices - select one', () => {
.children()
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
.and(($placeholder) => {
expect($placeholder).to.contain('I am a placeholder');
});
});
@ -577,7 +573,7 @@ describe('Choices - select one', () => {
.should('have.length', 1)
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
.and(($placeholder) => {
expect($placeholder).to.contain('Loading...');
});
});
@ -620,19 +616,17 @@ describe('Choices - select one', () => {
beforeEach(() => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .choices__item')
.then($choices => {
.then(($choices) => {
choicesCount = $choices.length;
});
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices')
.click();
cy.get('[data-test-hook=scrolling-dropdown]').find('.choices').click();
});
it('highlights first choice on dropdown open', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
@ -641,7 +635,7 @@ describe('Choices - select one', () => {
for (let index = 0; index < choicesCount; index++) {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index + 1}`);
});
@ -665,7 +659,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index}`);
});
@ -684,7 +678,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.then($group => {
.then(($group) => {
groupValue = $group.text().trim();
});
});
@ -705,7 +699,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.not.equal(groupValue);
});
});
@ -736,7 +730,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.equal(groupValue);
});
});
@ -806,9 +800,7 @@ describe('Choices - select one', () => {
describe('custom properties', () => {
beforeEach(() => {
cy.get('[data-test-hook=custom-properties]')
.find('.choices')
.click();
cy.get('[data-test-hook=custom-properties]').find('.choices').click();
});
describe('on input', () => {
@ -837,7 +829,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(city);
});
@ -851,9 +843,7 @@ describe('Choices - select one', () => {
describe('non-string values', () => {
beforeEach(() => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices')
.click();
cy.get('[data-test-hook=non-string-values]').find('.choices').click();
});
it('displays expected amount of choices in dropdown', () => {
@ -869,7 +859,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.then($choice => {
.then(($choice) => {
$selectedChoice = $choice;
})
.click();
@ -877,7 +867,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item.text().trim()).to.equal($selectedChoice.text().trim());
});
});
@ -887,7 +877,7 @@ describe('Choices - select one', () => {
describe('selecting choice', () => {
describe('on enter key', () => {
it('does not submit form', () => {
cy.get('[data-test-hook=within-form] form').then($form => {
cy.get('[data-test-hook=within-form] form').then(($form) => {
$form.submit(() => {
// this will fail the test if the form submits
throw new Error('Form submitted');
@ -900,14 +890,12 @@ describe('Choices - select one', () => {
.find('.choices__input--cloned')
.type('{enter}');
cy.get('[data-test-hook=within-form]')
.find('.choices')
.click();
cy.get('[data-test-hook=within-form]').find('.choices').click();
cy.get('[data-test-hook=within-form]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain('Choice 1');
});
});
@ -922,7 +910,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--single .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(
dynamicallySelectedChoiceValue,
);
@ -932,7 +920,7 @@ describe('Choices - select one', () => {
it('does not remove choice from dropdown list', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--dropdown .choices__list')
.then($choicesList => {
.then(($choicesList) => {
expect($choicesList).to.contain(dynamicallySelectedChoiceValue);
});
});
@ -940,7 +928,7 @@ describe('Choices - select one', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.contain(dynamicallySelectedChoiceValue);
});
@ -949,9 +937,7 @@ describe('Choices - select one', () => {
describe('searching by label only', () => {
beforeEach(() => {
cy.get('[data-test-hook=search-by-label]')
.find('.choices')
.click();
cy.get('[data-test-hook=search-by-label]').find('.choices').click();
});
it('gets zero results when searching by value', () => {
@ -963,7 +949,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('No results found');
});
});
@ -977,7 +963,7 @@ describe('Choices - select one', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('label1');
});
});
@ -998,7 +984,7 @@ describe('Choices - select one', () => {
.children()
.first()
.should('have.class', 'choices__item--disabled')
.then($choice => {
.then(($choice) => {
disabledValue = $choice.val();
});
});
@ -1006,19 +992,64 @@ describe('Choices - select one', () => {
it('selects the first enabled choice', () => {
cy.get('[data-test-hook=disabled-first-choice-via-options]')
.find('.choices__input[hidden]')
.then($option => {
.then(($option) => {
expect($option.text().trim()).to.not.equal(disabledValue);
});
cy.get('[data-test-hook=disabled-first-choice-via-options]')
.find('.choices__item.choices__item--selectable')
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.not.equal(disabledValue);
});
});
});
describe('allow html', () => {
describe('is undefined', () => {
it('logs a deprecation warning', () => {
cy.get('@consoleWarn').should(
'be.calledOnceWithExactly',
'Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.',
);
});
it('does not show html as text', () => {
cy.get('[data-test-hook=allowhtml-undefined]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
});
describe('set to true', () => {
it('does not show html as text', () => {
cy.get('[data-test-hook=allowhtml-true]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
});
describe('set to false', () => {
it('shows html as text', () => {
cy.get('[data-test-hook=allowhtml-false]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('<b>Choice 1</b>');
});
});
});
});
describe('re-initialising a choices instance', () => {
it('preserves the choices list', () => {
cy.get('[data-test-hook=new-destroy-init]')
@ -1029,9 +1060,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=new-destroy-init]')
.find('button.destroy')
.click();
cy.get('[data-test-hook=new-destroy-init]')
.find('button.init')
.click();
cy.get('[data-test-hook=new-destroy-init]').find('button.init').click();
cy.get('[data-test-hook=new-destroy-init]')
.find('.choices__list--dropdown .choices__list')

View File

@ -1,6 +1,10 @@
describe('Choices - text element', () => {
beforeEach(() => {
cy.visit('/text');
cy.visit('/text', {
onBeforeLoad(win) {
cy.stub(win.console, 'warn').as('consoleWarn');
},
});
});
describe('scenarios', () => {
@ -17,7 +21,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--multiple .choices__item')
.last()
.should($el => {
.should(($el) => {
expect($el).to.contain(textInput);
});
});
@ -42,7 +46,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
`Press Enter to add "${textInput}"`,
@ -74,7 +78,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=edit-items]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.data('value')).to.equal(`${textInput}-edited`);
});
});
@ -90,7 +94,7 @@ describe('Choices - text element', () => {
it('highlights all items', () => {
cy.get('[data-test-hook=edit-items]')
.find('.choices__list--multiple .choices__item')
.each($choice => {
.each(($choice) => {
expect($choice.hasClass('is-highlighted')).to.equal(true);
});
});
@ -124,7 +128,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__list--multiple')
.children()
.should($items => {
.should(($items) => {
expect($items.length).to.equal(1);
});
@ -137,7 +141,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__list--multiple .choices__item')
.should($items => {
.should(($items) => {
expect($items.length).to.equal(0);
});
});
@ -152,7 +156,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]')
.find('.choices__input[hidden]')
.then($input => {
.then(($input) => {
expect($input.val()).to.not.contain(textInput);
});
});
@ -175,7 +179,7 @@ describe('Choices - text element', () => {
.find('.choices__list--multiple')
.first()
.children()
.should($items => {
.should(($items) => {
expect($items.length).to.equal(1);
});
});
@ -185,7 +189,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=unique-values]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
'Only unique values can be added',
@ -212,7 +216,7 @@ describe('Choices - text element', () => {
.find('.choices__list--multiple')
.first()
.children()
.should($items => {
.should(($items) => {
expect($items.length).to.equal(inputLimit);
});
});
@ -222,7 +226,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=input-limit]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
`Only ${inputLimit} values can be added`,
@ -245,7 +249,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=add-item-filter]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(input);
});
});
@ -261,7 +265,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=add-item-filter]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
'Only values matching specific conditions can be added',
@ -283,7 +287,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.data('value')).to.equal(`before-${textInput}-after`);
});
});
@ -292,7 +296,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text()).to.not.contain(`before-${textInput}-after`);
expect($choice.text()).to.contain(textInput);
});
@ -319,21 +323,21 @@ describe('Choices - text element', () => {
it('pre-populates choices', () => {
cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item')
.should($choices => {
.should(($choices) => {
expect($choices.length).to.equal(2);
});
cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item')
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Josh Johnson');
});
cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Joe Bloggs');
});
});
@ -355,11 +359,53 @@ describe('Choices - text element', () => {
});
});
describe('allow html', () => {
describe('is undefined', () => {
it('logs a deprecation warning', () => {
cy.get('@consoleWarn').should(
'be.calledOnceWithExactly',
'Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.',
);
});
it('does not show html as text', () => {
cy.get('[data-test-hook=allowhtml-undefined]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Mason Rogers');
});
});
});
describe('set to true', () => {
it('does not show html as text', () => {
cy.get('[data-test-hook=allowhtml-true]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('Mason Rogers');
});
});
});
describe('set to false', () => {
it('shows html as text', () => {
cy.get('[data-test-hook=allowhtml-false]')
.find('.choices__list--multiple .choices__item')
.first()
.should(($choice) => {
expect($choice.text().trim()).to.equal('<b>Mason Rogers</b>');
});
});
});
});
describe('within form', () => {
describe('inputting item', () => {
describe('on enter key', () => {
it('does not submit form', () => {
cy.get('[data-test-hook=within-form] form').then($form => {
cy.get('[data-test-hook=within-form] form').then(($form) => {
$form.submit(() => {
// this will fail the test if the form submits
throw new Error('Form submitted');
@ -374,7 +420,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=within-form]')
.find('.choices__list--multiple .choices__item')
.last()
.should($el => {
.should(($el) => {
expect($el).to.contain(textInput);
});
});

View File

@ -1,4 +1,4 @@
/*! choices.js v9.0.1 | © 2021 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
/*! choices.js v9.1.0 | © 2021 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
@ -291,6 +291,10 @@ function () {
userConfig = {};
}
if (userConfig.allowHTML === undefined) {
console.warn('Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.');
}
this.config = deepmerge_1.default.all([defaults_1.DEFAULT_CONFIG, Choices.defaults.options, userConfig], // When merging array configs, replace with a copy of the userConfig array,
// instead of concatenating with the default array
{
@ -2209,8 +2213,7 @@ function () {
args[_i - 1] = arguments[_i];
}
var classNames = this.config.classNames;
return (_a = this._templates[template]).call.apply(_a, __spreadArray([this, classNames], args, false));
return (_a = this._templates[template]).call.apply(_a, __spreadArray([this, this.config], args, false));
};
Choices.prototype._createTemplates = function () {
@ -3483,6 +3486,7 @@ exports.DEFAULT_CONFIG = {
removeItems: true,
removeItemButton: false,
editItems: false,
allowHTML: true,
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,
@ -3644,7 +3648,7 @@ var sanitise = function (value) {
return value;
}
return value.replace(/&/g, '&amp;').replace(/>/g, '&rt;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
return value.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
};
exports.sanitise = sanitise;
@ -4376,7 +4380,7 @@ Object.defineProperty(exports, "__esModule", ({
}));
var templates = {
containerOuter: function (_a, dir, isSelectElement, isSelectOneElement, searchEnabled, passedElementType) {
var containerOuter = _a.containerOuter;
var containerOuter = _a.classNames.containerOuter;
var div = Object.assign(document.createElement('div'), {
className: containerOuter
});
@ -4403,32 +4407,39 @@ var templates = {
return div;
},
containerInner: function (_a) {
var containerInner = _a.containerInner;
var containerInner = _a.classNames.containerInner;
return Object.assign(document.createElement('div'), {
className: containerInner
});
},
itemList: function (_a, isSelectOneElement) {
var list = _a.list,
listSingle = _a.listSingle,
listItems = _a.listItems;
var _b = _a.classNames,
list = _b.list,
listSingle = _b.listSingle,
listItems = _b.listItems;
return Object.assign(document.createElement('div'), {
className: "".concat(list, " ").concat(isSelectOneElement ? listSingle : listItems)
});
},
placeholder: function (_a, value) {
var placeholder = _a.placeholder;
return Object.assign(document.createElement('div'), {
className: placeholder,
innerHTML: value
});
var _b;
var allowHTML = _a.allowHTML,
placeholder = _a.classNames.placeholder;
return Object.assign(document.createElement('div'), (_b = {
className: placeholder
}, _b[allowHTML ? 'innerHTML' : 'innerText'] = value, _b));
},
item: function (_a, _b, removeItemButton) {
var item = _a.item,
button = _a.button,
highlightedState = _a.highlightedState,
itemSelectable = _a.itemSelectable,
placeholder = _a.placeholder;
var _c, _d;
var allowHTML = _a.allowHTML,
_e = _a.classNames,
item = _e.item,
button = _e.button,
highlightedState = _e.highlightedState,
itemSelectable = _e.itemSelectable,
placeholder = _e.placeholder;
var id = _b.id,
value = _b.value,
label = _b.label,
@ -4437,10 +4448,9 @@ var templates = {
disabled = _b.disabled,
highlighted = _b.highlighted,
isPlaceholder = _b.placeholder;
var div = Object.assign(document.createElement('div'), {
className: item,
innerHTML: label
});
var div = Object.assign(document.createElement('div'), (_c = {
className: item
}, _c[allowHTML ? 'innerHTML' : 'innerText'] = label, _c));
Object.assign(div.dataset, {
item: '',
id: id,
@ -4471,11 +4481,10 @@ var templates = {
/** @todo This MUST be localizable, not hardcoded! */
var REMOVE_ITEM_TEXT = 'Remove item';
var removeButton = Object.assign(document.createElement('button'), {
var removeButton = Object.assign(document.createElement('button'), (_d = {
type: 'button',
className: button,
innerHTML: REMOVE_ITEM_TEXT
});
className: button
}, _d[allowHTML ? 'innerHTML' : 'innerText'] = REMOVE_ITEM_TEXT, _d));
removeButton.setAttribute('aria-label', "".concat(REMOVE_ITEM_TEXT, ": '").concat(value, "'"));
removeButton.dataset.button = '';
div.appendChild(removeButton);
@ -4484,7 +4493,7 @@ var templates = {
return div;
},
choiceList: function (_a, isSelectOneElement) {
var list = _a.list;
var list = _a.classNames.list;
var div = Object.assign(document.createElement('div'), {
className: list
});
@ -4497,9 +4506,13 @@ var templates = {
return div;
},
choiceGroup: function (_a, _b) {
var group = _a.group,
groupHeading = _a.groupHeading,
itemDisabled = _a.itemDisabled;
var _c;
var allowHTML = _a.allowHTML,
_d = _a.classNames,
group = _d.group,
groupHeading = _d.groupHeading,
itemDisabled = _d.itemDisabled;
var id = _b.id,
value = _b.value,
disabled = _b.disabled;
@ -4517,19 +4530,22 @@ var templates = {
div.setAttribute('aria-disabled', 'true');
}
div.appendChild(Object.assign(document.createElement('div'), {
className: groupHeading,
innerHTML: value
}));
div.appendChild(Object.assign(document.createElement('div'), (_c = {
className: groupHeading
}, _c[allowHTML ? 'innerHTML' : 'innerText'] = value, _c)));
return div;
},
choice: function (_a, _b, selectText) {
var item = _a.item,
itemChoice = _a.itemChoice,
itemSelectable = _a.itemSelectable,
selectedState = _a.selectedState,
itemDisabled = _a.itemDisabled,
placeholder = _a.placeholder;
var _c;
var allowHTML = _a.allowHTML,
_d = _a.classNames,
item = _d.item,
itemChoice = _d.itemChoice,
itemSelectable = _d.itemSelectable,
selectedState = _d.selectedState,
itemDisabled = _d.itemDisabled,
placeholder = _d.placeholder;
var id = _b.id,
value = _b.value,
label = _b.label,
@ -4538,11 +4554,9 @@ var templates = {
isDisabled = _b.disabled,
isSelected = _b.selected,
isPlaceholder = _b.placeholder;
var div = Object.assign(document.createElement('div'), {
id: elementId,
innerHTML: label,
className: "".concat(item, " ").concat(itemChoice)
});
var div = Object.assign(document.createElement('div'), (_c = {
id: elementId
}, _c[allowHTML ? 'innerHTML' : 'innerText'] = label, _c.className = "".concat(item, " ").concat(itemChoice), _c));
if (isSelected) {
div.classList.add(selectedState);
@ -4572,10 +4586,12 @@ var templates = {
return div;
},
input: function (_a, placeholderValue) {
var input = _a.input,
inputCloned = _a.inputCloned;
var _b = _a.classNames,
input = _b.input,
inputCloned = _b.inputCloned;
var inp = Object.assign(document.createElement('input'), {
type: 'text',
type: 'search',
name: 'search_terms',
className: "".concat(input, " ").concat(inputCloned),
autocomplete: 'off',
autocapitalize: 'off',
@ -4587,18 +4603,23 @@ var templates = {
return inp;
},
dropdown: function (_a) {
var list = _a.list,
listDropdown = _a.listDropdown;
var _b = _a.classNames,
list = _b.list,
listDropdown = _b.listDropdown;
var div = document.createElement('div');
div.classList.add(list, listDropdown);
div.setAttribute('aria-expanded', 'false');
return div;
},
notice: function (_a, innerHTML, type) {
var item = _a.item,
itemChoice = _a.itemChoice,
noResults = _a.noResults,
noChoices = _a.noChoices;
notice: function (_a, innerText, type) {
var _b;
var allowHTML = _a.allowHTML,
_c = _a.classNames,
item = _c.item,
itemChoice = _c.itemChoice,
noResults = _c.noResults,
noChoices = _c.noChoices;
if (type === void 0) {
type = '';
@ -4612,10 +4633,7 @@ var templates = {
classes.push(noResults);
}
return Object.assign(document.createElement('div'), {
innerHTML: innerHTML,
className: classes.join(' ')
});
return Object.assign(document.createElement('div'), (_b = {}, _b[allowHTML ? 'innerHTML' : 'innerText'] = innerText, _b.className = classes.join(' '), _b));
},
option: function (_a) {
var label = _a.label,

File diff suppressed because one or more lines are too long

View File

@ -534,6 +534,7 @@
for (i = 0; i < genericExamples.length; ++i) {
var element = genericExamples[i];
new Choices(element, {
allowHTML: true,
placeholderValue: 'This is a placeholder set in the config',
searchPlaceholderValue: 'This is a search placeholder',
});
@ -542,6 +543,7 @@
var textRemove = new Choices(
document.getElementById('choices-text-remove-button'),
{
allowHTML: true,
delimiter: ',',
editItems: true,
maxItemCount: 5,
@ -550,12 +552,14 @@
);
var textUniqueVals = new Choices('#choices-text-unique-values', {
allowHTML: true,
paste: false,
duplicateItemsAllowed: false,
editItems: true,
});
var texti18n = new Choices('#choices-text-i18n', {
allowHTML: true,
paste: false,
duplicateItemsAllowed: false,
editItems: true,
@ -572,6 +576,7 @@
});
var textEmailFilter = new Choices('#choices-text-email-filter', {
allowHTML: true,
editItems: true,
addItemFilter: function(value) {
if (!value) {
@ -585,6 +590,7 @@
}).setValue(['joe@bloggs.com']);
var textDisabled = new Choices('#choices-text-disabled', {
allowHTML: true,
addItems: false,
removeItems: false,
}).disable();
@ -592,12 +598,14 @@
var textPrependAppendVal = new Choices(
'#choices-text-prepend-append-value',
{
allowHTML: true,
prependValue: 'item-',
appendValue: '-' + Date.now(),
}
).removeActiveItems();
var textPresetVal = new Choices('#choices-text-preset-values', {
allowHTML: true,
items: [
'Josh Johnson',
{
@ -611,10 +619,12 @@
});
var multipleDefault = new Choices(
document.getElementById('choices-multiple-groups')
document.getElementById('choices-multiple-groups'),
{ allowHTML: true }
);
var multipleFetch = new Choices('#choices-multiple-remote-fetch', {
allowHTML: false,
placeholder: true,
placeholderValue: 'Pick an Strokes record',
maxItemCount: 5,
@ -635,12 +645,14 @@
var multipleCancelButton = new Choices(
'#choices-multiple-remove-button',
{
allowHTML: true,
removeItemButton: true,
}
);
/* Use label on event */
var choicesSelect = new Choices('#choices-multiple-labels', {
allowHTML: true,
removeItemButton: true,
choices: [
{ value: 'One', label: 'Label One' },
@ -675,6 +687,7 @@
);
var singleFetch = new Choices('#choices-single-remote-fetch', {
allowHTML: false,
searchPlaceholderValue: 'Search for an Arctic Monkeys record',
})
.setChoices(function() {
@ -695,6 +708,7 @@
});
var singleXhrRemove = new Choices('#choices-single-remove-xhr', {
allowHTML: true,
removeItemButton: true,
searchPlaceholderValue: "Search for a Smiths' record",
}).setChoices(function(callback) {
@ -712,6 +726,7 @@
});
var singleNoSearch = new Choices('#choices-single-no-search', {
allowHTML: true,
searchEnabled: false,
removeItemButton: true,
choices: [
@ -731,6 +746,7 @@
);
var singlePresetOpts = new Choices('#choices-single-preset-options', {
allowHTML: true,
placeholder: true,
}).setChoices(
[
@ -760,6 +776,7 @@
);
var singleSelectedOpt = new Choices('#choices-single-selected-option', {
allowHTML: true,
searchFields: ['label', 'value', 'customProperties.description'],
choices: [
{ value: 'One', label: 'Label One', selected: true },
@ -777,17 +794,20 @@
var customChoicesPropertiesViaDataAttributes = new Choices(
'#choices-with-custom-props-via-html',
{
allowHTML: true,
searchFields: ['label', 'value', 'customProperties'],
}
);
var singleNoSorting = new Choices('#choices-single-no-sorting', {
allowHTML: true,
shouldSort: false,
});
var cities = new Choices(document.getElementById('cities'));
var cities = new Choices(document.getElementById('cities'), { allowHTML: true });
var tubeStations = new Choices(
document.getElementById('tube-stations')
document.getElementById('tube-stations'),
{ allowHTML: true }
).disable();
cities.passedElement.element.addEventListener('change', function(e) {
@ -801,11 +821,12 @@
var customTemplates = new Choices(
document.getElementById('choices-single-custom-templates'),
{
allowHTML: true,
callbackOnCreateTemplates: function(strToEl) {
var classNames = this.config.classNames;
var itemSelectText = this.config.itemSelectText;
return {
item: function(classNames, data) {
item: function({ classNames }, data) {
return strToEl(
'\
<div\
@ -839,7 +860,7 @@
'
);
},
choice: function(classNames, data) {
choice: function({ classNames }, data) {
return strToEl(
'\
<div\
@ -889,9 +910,12 @@
}
);
var resetSimple = new Choices(document.getElementById('reset-simple'));
var resetSimple = new Choices(document.getElementById('reset-simple'), {
allowHTML: true,
});
var resetMultiple = new Choices('#reset-multiple', {
allowHTML: true,
removeItemButton: true,
});
});

View File

@ -345,11 +345,43 @@
<option value="value2">label2</option>
</select>
</div>
<div data-test-hook="allowhtml-undefined">
<label for="choices-allowhtml-undefined">HTML allowed by default</label>
<select
class="form-control"
name="choices-allowhtml-undefined"
id="choices-allowhtml-undefined"
multiple
></select>
</div>
<div data-test-hook="allowhtml-true">
<label for="choices-allowhtml-true">HTML allowed</label>
<select
class="form-control"
name="choices-allowhtml-true"
id="choices-allowhtml-true"
multiple
></select>
</div>
<div data-test-hook="allowhtml-false">
<label for="choices-allowhtml-false">HTML disabled</label>
<select
class="form-control"
name="choices-allowhtml-false"
id="choices-allowhtml-false"
multiple
></select>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
const choicesBasic = new Choices('#choices-basic', {
allowHTML: true,
});
document
.querySelector('button.disable')
@ -364,39 +396,54 @@
});
new Choices('#choices-remove-button', {
allowHTML: true,
removeItemButton: true,
});
new Choices('#choices-disabled-choice');
new Choices('#choices-disabled-choice', {
allowHTML: true,
});
new Choices('#choices-add-items-disabled', {
allowHTML: true,
addItems: false,
});
new Choices('#choices-disabled-via-attr');
new Choices('#choices-disabled-via-attr', {
allowHTML: true,
});
new Choices('#choices-selection-limit', {
allowHTML: true,
maxItemCount: 5,
});
new Choices('#choices-prepend-append', {
allowHTML: true,
prependValue: 'before-',
appendValue: '-after',
});
new Choices('#choices-render-choice-limit', {
allowHTML: true,
renderChoiceLimit: 1,
});
new Choices('#choices-search-floor', {
allowHTML: true,
searchFloor: 5,
});
new Choices('#choices-placeholder-via-option-value');
new Choices('#choices-placeholder-via-option-value', {
allowHTML: true,
});
new Choices('#choices-placeholder-via-option-attr');
new Choices('#choices-placeholder-via-option-attr', {
allowHTML: true,
});
new Choices('#choices-remote-data', {
allowHTML: true,
shouldSort: false,
}).setChoices(async () => {
const data = await fetch('/data');
@ -404,12 +451,16 @@
});
new Choices('#choices-scrolling-dropdown', {
allowHTML: true,
shouldSort: false,
});
new Choices('#choices-groups');
new Choices('#choices-groups', {
allowHTML: true,
});
new Choices('#choices-custom-properties', {
allowHTML: true,
searchFields: ['label', 'value', 'customProperties.country'],
choices: [
{
@ -440,6 +491,7 @@
});
new Choices('#choices-non-string-values', {
allowHTML: true,
choices: [
{
id: 1,
@ -466,13 +518,83 @@
],
});
new Choices('#choices-within-form');
new Choices('#choices-within-form', {
allowHTML: true,
});
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-set-choice-by-value', {
allowHTML: true,
}).setChoiceByValue('Choice 2');
new Choices('#choices-search-by-label', { searchFields: ['label'] });
new Choices('#choices-search-by-label', {
allowHTML: true,
searchFields: ['label']
});
new Choices('#choices-allowhtml-undefined', {
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
selected: true
},
{
id: 2,
label: '<b>Choice 2</b>',
value: 'Choice 2',
},
{
id: 3,
label: 'Choice 3',
value: 'Choice 3',
},
],
});
new Choices('#choices-allowhtml-true', {
allowHTML: true,
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
selected: true
},
{
id: 2,
label: '<b>Choice 2</b>',
value: 'Choice 2',
},
{
id: 3,
label: 'Choice 3',
value: 'Choice 3',
},
],
});
new Choices('#choices-allowhtml-false', {
allowHTML: false,
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
selected: true
},
{
id: 2,
label: '<b>Choice 2</b>',
value: 'Choice 2',
},
{
id: 3,
label: 'Choice 3',
value: 'Choice 3',
},
],
});
});
</script>
</body>

View File

@ -354,6 +354,33 @@
</select>
</div>
<div data-test-hook="allowhtml-undefined">
<label for="choices-allowhtml-undefined">HTML allowed by default</label>
<select
class="form-control"
name="choices-allowhtml-undefined"
id="choices-allowhtml-undefined"
></select>
</div>
<div data-test-hook="allowhtml-true">
<label for="choices-allowhtml-true">HTML allowed</label>
<select
class="form-control"
name="choices-allowhtml-true"
id="choices-allowhtml-true"
></select>
</div>
<div data-test-hook="allowhtml-false">
<label for="choices-allowhtml-false">HTML disabled</label>
<select
class="form-control"
name="choices-allowhtml-false"
id="choices-allowhtml-false"
></select>
</div>
<div data-test-hook="new-destroy-init">
<label for="choices-new-destroy-init">New, Destroy, Init</label>
<select
@ -372,7 +399,9 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
const choicesBasic = new Choices('#choices-basic', {
allowHTML: true,
});
document
.querySelector('button.disable')
@ -387,14 +416,17 @@
});
new Choices('#choices-remove-button', {
allowHTML: true,
removeItemButton: true,
});
new Choices('#choices-disabled-choice', {
allowHTML: true,
removeItemButton: true,
});
new Choices('#choices-disabled-choice-via-options', {
allowHTML: true,
removeItemButton: true,
choices: [
{
@ -418,33 +450,45 @@
});
new Choices('#choices-add-items-disabled', {
allowHTML: true,
addItems: false,
});
new Choices('#choices-disabled-via-attr');
new Choices('#choices-disabled-via-attr', {
allowHTML: true,
});
new Choices('#choices-prepend-append', {
allowHTML: true,
prependValue: 'before-',
appendValue: '-after',
});
new Choices('#choices-render-choice-limit', {
allowHTML: true,
renderChoiceLimit: 1,
});
new Choices('#choices-search-disabled', {
allowHTML: true,
searchEnabled: false,
});
new Choices('#choices-search-floor', {
allowHTML: true,
searchFloor: 5,
});
new Choices('#choices-placeholder-via-option-value');
new Choices('#choices-placeholder-via-option-value', {
allowHTML: true,
});
new Choices('#choices-placeholder-via-option-attr');
new Choices('#choices-placeholder-via-option-attr', {
allowHTML: true,
});
new Choices('#choices-remote-data', {
allowHTML: true,
shouldSort: false,
}).setChoices(async () => {
const res = await fetch('/data');
@ -452,13 +496,20 @@
});
new Choices('#choices-scrolling-dropdown', {
allowHTML: true,
shouldSort: false,
});
new Choices('#choices-groups');
new Choices('#choices-groups', {
allowHTML: true,
});
const parent = new Choices('#choices-parent');
const child = new Choices('#choices-child').disable();
const parent = new Choices('#choices-parent', {
allowHTML: true,
});
const child = new Choices('#choices-child', {
allowHTML: true,
}).disable();
parent.passedElement.element.addEventListener('change', event => {
if (event.detail.value === 'Parent choice 2') {
@ -469,6 +520,7 @@
});
new Choices('#choices-custom-properties', {
allowHTML: true,
searchFields: ['label', 'value', 'customProperties.country'],
choices: [
{
@ -499,6 +551,7 @@
});
new Choices('#choices-non-string-values', {
allowHTML: true,
choices: [
{
id: 1,
@ -525,15 +578,69 @@
],
});
new Choices('#choices-within-form');
new Choices('#choices-within-form', {
allowHTML: true,
});
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-set-choice-by-value', {
allowHTML: true,
}).setChoiceByValue('Choice 2');
new Choices('#choices-search-by-label', { searchFields: ['label'] });
new Choices('#choices-search-by-label', {
allowHTML: true,
searchFields: ['label']
});
const newDestroyInitChoices = new Choices('#choices-new-destroy-init');
new Choices('#choices-allowhtml-undefined', {
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
},
{
id: 2,
label: 'Choice 2',
value: 'Choice 2',
},
],
});
new Choices('#choices-allowhtml-true', {
allowHTML: true,
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
},
{
id: 2,
label: 'Choice 2',
value: 'Choice 2',
},
],
});
new Choices('#choices-allowhtml-false', {
allowHTML: false,
choices: [
{
id: 1,
label: '<b>Choice 1</b>',
value: 'Choice 1',
},
{
id: 2,
label: 'Choice 2',
value: 'Choice 2',
},
],
});
const newDestroyInitChoices = new Choices('#choices-new-destroy-init', {
allowHTML: true,
});
document
.querySelector('button.destroy')
.addEventListener('click', () => {

View File

@ -76,6 +76,21 @@
<input class="form-control" id="choices-unique-values" type="text" />
</div>
<div data-test-hook="allowhtml-undefined">
<label for="allowhtml-undefined">HTML allowed by default</label>
<input class="form-control" id="allowhtml-undefined" type="text" />
</div>
<div data-test-hook="allowhtml-true">
<label for="allowhtml-true">HTML allowed</label>
<input class="form-control" id="allowhtml-true" type="text" />
</div>
<div data-test-hook="allowhtml-false">
<label for="allowhtml-false">HTML disabled</label>
<input class="form-control" id="allowhtml-false" type="text" />
</div>
<div data-test-hook="input-limit">
<label for="choices-input-limit">Input limit</label>
<input class="form-control" id="choices-input-limit" type="text" />
@ -134,25 +149,52 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
new Choices('#choices-basic');
new Choices('#choices-basic', {
allowHTML: true,
});
new Choices('#choices-edit-items', {
allowHTML: true,
editItems: true,
});
new Choices('#choices-remove-button', {
allowHTML: true,
removeItemButton: true,
});
new Choices('#choices-unique-values', {
allowHTML: true,
duplicateItemsAllowed: false,
});
new Choices('#allowhtml-undefined', {
items: [
'<b>Mason Rogers</b>'
],
});
new Choices('#allowhtml-true', {
allowHTML: true,
items: [
'<b>Mason Rogers</b>'
],
});
new Choices('#allowhtml-false', {
allowHTML: false,
items: [
'<b>Mason Rogers</b>'
],
});
new Choices('#choices-input-limit', {
allowHTML: true,
maxItemCount: 5,
});
new Choices('#choices-add-item-filter', {
allowHTML: true,
addItems: true,
addItemFilter: value => {
const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@ -162,17 +204,22 @@
});
new Choices('#choices-adding-items-disabled', {
allowHTML: true,
addItems: false,
});
new Choices('#choices-disabled-via-attr');
new Choices('#choices-disabled-via-attr', {
allowHTML: true,
});
new Choices('#choices-prepend-append', {
allowHTML: true,
prependValue: 'before-',
appendValue: '-after',
});
new Choices('#choices-prepopulated', {
allowHTML: true,
items: [
'Josh Johnson',
{
@ -186,11 +233,14 @@
});
new Choices('#choices-placeholder', {
allowHTML: true,
placeholder: true,
placeholderValue: 'I am a placeholder',
});
new Choices('#choices-within-form');
new Choices('#choices-within-form', {
allowHTML: true,
});
});
</script>
</body>

View File

@ -26,7 +26,7 @@ describe('choices', () => {
passedElement.className = 'js-choices';
document.body.appendChild(passedElement);
instance = new Choices(passedElement);
instance = new Choices(passedElement, { allowHTML: true });
});
afterEach(() => {
@ -55,6 +55,7 @@ describe('choices', () => {
`;
const config = {
allowHTML: true,
renderChoiceLimit: 5,
};
instance = new Choices('[data-choice]', config);
@ -73,6 +74,7 @@ describe('choices', () => {
`;
instance = new Choices('[data-choice]', {
allowHTML: true,
searchEnabled: false,
});
@ -88,6 +90,7 @@ describe('choices', () => {
`;
instance = new Choices('[data-choice]', {
allowHTML: true,
renderSelectedChoices: 'test' as any,
});
@ -108,7 +111,7 @@ describe('choices', () => {
const inputs = document.querySelectorAll('[data-choice]');
expect(inputs.length).to.equal(3);
instance = new Choices();
instance = new Choices(undefined, { allowHTML: true });
expect(instance.passedElement.element.id).to.equal(inputs[0].id);
});
@ -116,7 +119,7 @@ describe('choices', () => {
describe('when an element cannot be found in the DOM', () => {
it('throws an error', () => {
document.body.innerHTML = ``;
expect(() => new Choices()).to.throw(
expect(() => new Choices(undefined, { allowHTML: true })).to.throw(
TypeError,
'Expected one of the following types text|select-one|select-multiple',
);
@ -133,7 +136,7 @@ describe('choices', () => {
});
it('sets the initialised flag to true', () => {
instance = new Choices('#input-1');
instance = new Choices('#input-1', { allowHTML: true });
expect(instance.initialised).to.equal(true);
});
@ -141,6 +144,7 @@ describe('choices', () => {
const initSpy = spy();
// initialise with the same element
instance = new Choices('#input-1', {
allowHTML: true,
silent: true,
callbackOnInit: initSpy,
});
@ -156,12 +160,12 @@ describe('choices', () => {
`;
// initialise once
new Choices('#input-1', { silent: true });
new Choices('#input-1', { allowHTML: true, silent: true });
});
it('sets the initialised flag to true', () => {
// initialise with the same element
instance = new Choices('#input-1', { silent: true });
instance = new Choices('#input-1', { allowHTML: true, silent: true });
expect(instance.initialised).to.equal(true);
});
@ -170,6 +174,7 @@ describe('choices', () => {
const initSpy = spy();
// initialise with the same element
instance = new Choices('#input-1', {
allowHTML: true,
silent: true,
callbackOnInit: initSpy,
});
@ -185,7 +190,7 @@ describe('choices', () => {
<input data-choice type="text" id="input-1" />
`;
instance = new Choices('[data-choice]');
instance = new Choices('[data-choice]', { allowHTML: true });
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
});
@ -197,7 +202,7 @@ describe('choices', () => {
<select data-choice id="select-1"></select>
`;
instance = new Choices('[data-choice]');
instance = new Choices('[data-choice]', { allowHTML: true });
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
});
@ -211,7 +216,7 @@ describe('choices', () => {
<input data-choice type="text" id="input-1" />
`;
instance = new Choices('[data-choice]');
instance = new Choices('[data-choice]', { allowHTML: true });
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
});
@ -223,7 +228,7 @@ describe('choices', () => {
<select data-choice id="select-1"></select>
`;
instance = new Choices('[data-choice]');
instance = new Choices('[data-choice]', { allowHTML: true });
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
});
@ -235,7 +240,9 @@ describe('choices', () => {
document.body.innerHTML = `
<div data-choice id="div-1"></div>
`;
expect(() => new Choices('[data-choice]')).to.throw(
expect(
() => new Choices('[data-choice]', { allowHTML: true }),
).to.throw(
TypeError,
'Expected one of the following types text|select-one|select-multiple',
);
@ -250,6 +257,7 @@ describe('choices', () => {
beforeEach(() => {
instance = new Choices(passedElement, {
allowHTML: true,
callbackOnInit: callbackOnInitSpy,
silent: true,
});
@ -330,7 +338,7 @@ describe('choices', () => {
passedElement.className = 'js-choices';
document.body.appendChild(passedElement);
instance = new Choices(passedElement);
instance = new Choices(passedElement, { allowHTML: true });
});
describe('not already initialised', () => {
@ -1188,7 +1196,7 @@ describe('choices', () => {
describe('select element', () => {
it('fetches and sets choices', async () => {
document.body.innerHTML = '<select id="test" />';
const choice = new Choices('#test');
const choice = new Choices('#test', { allowHTML: true });
const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
let fetcherCalled = false;
@ -2063,7 +2071,7 @@ describe('choices', () => {
output = instance._getTemplate(templateKey, customArg);
expect(output).to.deep.equal(element);
expect(instance._templates[templateKey]).to.have.been.calledOnceWith(
instance.config.classNames,
instance.config,
customArg,
);
});

View File

@ -151,6 +151,12 @@ class Choices implements Choices {
| HTMLSelectElement = '[data-choice]',
userConfig: Partial<Options> = {},
) {
if (userConfig.allowHTML === undefined) {
console.warn(
'Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.',
);
}
this.config = merge.all<Options>(
[DEFAULT_CONFIG, Choices.defaults.options, userConfig],
// When merging array configs, replace with a copy of the userConfig array,
@ -2105,9 +2111,7 @@ class Choices implements Choices {
}
_getTemplate(template: string, ...args: any): any {
const { classNames } = this.config;
return this._templates[template].call(this, classNames, ...args);
return this._templates[template].call(this, this.config, ...args);
}
_createTemplates(): void {

View File

@ -54,6 +54,7 @@ describe('constants', () => {
expect(DEFAULT_CONFIG.removeItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.removeItemButton).to.be.a('boolean');
expect(DEFAULT_CONFIG.editItems).to.be.a('boolean');
expect(DEFAULT_CONFIG.allowHTML).to.be.a('boolean');
expect(DEFAULT_CONFIG.duplicateItemsAllowed).to.be.a('boolean');
expect(DEFAULT_CONFIG.delimiter).to.be.a('string');
expect(DEFAULT_CONFIG.paste).to.be.a('boolean');

View File

@ -42,6 +42,7 @@ export const DEFAULT_CONFIG: Options = {
removeItems: true,
removeItemButton: false,
editItems: false,
allowHTML: true,
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,

View File

@ -159,6 +159,19 @@ export interface Options {
*/
editItems: boolean;
/**
* Whether HTML should be rendered in all Choices elements.
* If `false`, all elements (placeholder, items, etc.) will be treated as plain text.
* If `true`, this can be used to perform XSS scripting attacks if you load choices from a remote source.
*
* **Deprecation Warning:** This will default to `false` in a future release.
*
* **Input types affected:** text, select-one, select-multiple
*
* @default true
*/
allowHTML: boolean;
/**
* Whether each inputted/chosen item should be unique.
*

View File

@ -1,6 +1,9 @@
import { expect } from 'chai';
import templates from './templates';
import { strToEl } from './lib/utils';
import { DEFAULT_CLASSNAMES, DEFAULT_CONFIG } from './defaults';
import { Options } from './interfaces/options';
import { ClassNames } from './interfaces/class-names';
/**
* @param {HTMLElement} element1
@ -21,11 +24,25 @@ function expectEqualElements(element1, element2): void {
}
}
function createOptionsWithPartialClasses(
classNames: Partial<ClassNames>,
options: Partial<Options> = {},
): Options {
return {
...DEFAULT_CONFIG,
...options,
classNames: {
...DEFAULT_CLASSNAMES,
...classNames,
},
};
}
describe('templates', () => {
describe('containerOuter', () => {
const classes = {
const options = createOptionsWithPartialClasses({
containerOuter: 'class-1',
};
});
const direction = 'rtl';
describe('select element', () => {
@ -38,7 +55,7 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.containerOuter}"
class="${options.classNames.containerOuter}"
data-type="${passedElementType}"
role="combobox"
aria-autocomplete="list"
@ -49,7 +66,7 @@ describe('templates', () => {
</div>
`);
const actualOutput = templates.containerOuter(
classes,
options,
direction,
isSelectElement,
isSelectOneElement,
@ -69,7 +86,7 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.containerOuter}"
class="${options.classNames.containerOuter}"
data-type="${passedElementType}"
role="listbox"
aria-haspopup="true"
@ -79,7 +96,7 @@ describe('templates', () => {
</div>
`);
const actualOutput = templates.containerOuter(
classes,
options,
direction,
isSelectElement,
isSelectOneElement,
@ -100,7 +117,7 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.containerOuter}"
class="${options.classNames.containerOuter}"
data-type="${passedElementType}"
role="listbox"
tabindex="0"
@ -111,7 +128,7 @@ describe('templates', () => {
</div>
`);
const actualOutput = templates.containerOuter(
classes,
options,
direction,
isSelectElement,
isSelectOneElement,
@ -133,7 +150,7 @@ describe('templates', () => {
const expectedOutput = strToEl(`
<div
class="${classes.containerOuter}"
class="${options.classNames.containerOuter}"
data-type="${passedElementType}"
aria-haspopup="true"
aria-expanded="false"
@ -142,7 +159,7 @@ describe('templates', () => {
</div>
`);
const actualOutput = templates.containerOuter(
classes,
options,
direction,
isSelectElement,
isSelectOneElement,
@ -157,31 +174,31 @@ describe('templates', () => {
describe('containerInner', () => {
it('returns expected html', () => {
const classes = {
const innerOptions = createOptionsWithPartialClasses({
containerInner: 'class-1',
};
});
const expectedOutput = strToEl(
`<div class="${classes.containerInner}"></div>`,
`<div class="${innerOptions.classNames.containerInner}"></div>`,
);
const actualOutput = templates.containerInner(classes);
const actualOutput = templates.containerInner(innerOptions);
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('itemList', () => {
const classes = {
const itemOptions = createOptionsWithPartialClasses({
list: 'class-1',
listSingle: 'class-2',
listItems: 'class-3',
};
});
describe('select one element', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listSingle}"></div>`,
`<div class="${itemOptions.classNames.list} ${itemOptions.classNames.listSingle}"></div>`,
);
const actualOutput = templates.itemList(classes, true);
const actualOutput = templates.itemList(itemOptions, true);
expectEqualElements(actualOutput, expectedOutput);
});
@ -190,9 +207,9 @@ describe('templates', () => {
describe('non select one element', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listItems}"></div>`,
`<div class="${itemOptions.classNames.list} ${itemOptions.classNames.listItems}"></div>`,
);
const actualOutput = templates.itemList(classes, false);
const actualOutput = templates.itemList(itemOptions, false);
expectEqualElements(actualOutput, expectedOutput);
});
@ -201,33 +218,33 @@ describe('templates', () => {
describe('placeholder', () => {
it('returns expected html', () => {
const classes = {
const placeholderOptions = createOptionsWithPartialClasses({
placeholder: 'class-1',
};
});
const value = 'test';
const expectedOutput = strToEl(`
<div class="${classes.placeholder}">${value}</div>`);
const actualOutput = templates.placeholder(classes, value);
<div class="${placeholderOptions.classNames.placeholder}">${value}</div>`);
const actualOutput = templates.placeholder(placeholderOptions, value);
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('choiceList', () => {
const classes = {
const choiceListOptions = createOptionsWithPartialClasses({
list: 'class-1',
};
});
describe('select one element', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.list}"
class="${choiceListOptions.classNames.list}"
role="listbox"
>
</div>
`);
const actualOutput = templates.choiceList(classes, true);
const actualOutput = templates.choiceList(choiceListOptions, true);
expectEqualElements(actualOutput, expectedOutput);
});
@ -237,13 +254,13 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.list}"
class="${choiceListOptions.classNames.list}"
role="listbox"
aria-multiselectable="true"
>
</div>
`);
const actualOutput = templates.choiceList(classes, false);
const actualOutput = templates.choiceList(choiceListOptions, false);
expectEqualElements(actualOutput, expectedOutput);
});
@ -251,11 +268,11 @@ describe('templates', () => {
});
describe('choiceGroup', () => {
const classes = {
const groupOptions = createOptionsWithPartialClasses({
group: 'class-1',
groupHeading: 'class-2',
itemDisabled: 'class-3',
};
});
let data;
@ -271,16 +288,16 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.group}"
class="${groupOptions.classNames.group}"
data-group
data-id="${data.id}"
data-value="${data.value}"
role="group"
>
<div class="${classes.groupHeading}">${data.value}</div>
<div class="${groupOptions.classNames.groupHeading}">${data.value}</div>
</div>
`);
const actualOutput = templates.choiceGroup(classes, data);
const actualOutput = templates.choiceGroup(groupOptions, data);
expectEqualElements(actualOutput, expectedOutput);
});
@ -297,17 +314,17 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.group} ${classes.itemDisabled}"
class="${groupOptions.classNames.group} ${groupOptions.classNames.itemDisabled}"
data-group
data-id="${data.id}"
data-value="${data.value}"
role="group"
aria-disabled="true"
>
<div class="${classes.groupHeading}">${data.value}</div>
<div class="${groupOptions.classNames.groupHeading}">${data.value}</div>
</div>
`);
const actualOutput = templates.choiceGroup(classes, data);
const actualOutput = templates.choiceGroup(groupOptions, data);
expectEqualElements(actualOutput, expectedOutput);
});
@ -315,14 +332,14 @@ describe('templates', () => {
});
describe('choice', () => {
const classes = {
const choiceOptions = createOptionsWithPartialClasses({
item: 'class-1',
itemChoice: 'class-2',
itemDisabled: 'class-3',
itemSelectable: 'class-4',
placeholder: 'class-5',
selectedState: 'class-6',
};
});
const itemSelectText = 'test 6';
@ -344,7 +361,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${classes.itemSelectable}"
class="${choiceOptions.classNames.item} ${choiceOptions.classNames.itemChoice} ${choiceOptions.classNames.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -356,7 +373,11 @@ describe('templates', () => {
${data.label}
</div>
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
const actualOutput = templates.choice(
choiceOptions,
data,
itemSelectText,
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -373,7 +394,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${classes.itemDisabled}"
class="${choiceOptions.classNames.item} ${choiceOptions.classNames.itemChoice} ${choiceOptions.classNames.itemDisabled}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -386,7 +407,11 @@ describe('templates', () => {
${data.label}
</div>
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
const actualOutput = templates.choice(
choiceOptions,
data,
itemSelectText,
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -403,7 +428,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${classes.selectedState} ${classes.itemSelectable}"
class="${choiceOptions.classNames.item} ${choiceOptions.classNames.itemChoice} ${choiceOptions.classNames.selectedState} ${choiceOptions.classNames.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -415,7 +440,11 @@ describe('templates', () => {
${data.label}
</div>
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
const actualOutput = templates.choice(
choiceOptions,
data,
itemSelectText,
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -432,7 +461,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${classes.placeholder} ${classes.itemSelectable}"
class="${choiceOptions.classNames.item} ${choiceOptions.classNames.itemChoice} ${choiceOptions.classNames.placeholder} ${choiceOptions.classNames.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -444,7 +473,11 @@ describe('templates', () => {
${data.label}
</div>
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
const actualOutput = templates.choice(
choiceOptions,
data,
itemSelectText,
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -461,7 +494,7 @@ describe('templates', () => {
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div
class="${classes.item} ${classes.itemChoice} ${classes.itemSelectable}"
class="${choiceOptions.classNames.item} ${choiceOptions.classNames.itemChoice} ${choiceOptions.classNames.itemSelectable}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
@ -473,7 +506,11 @@ describe('templates', () => {
${data.label}
</div>
`);
const actualOutput = templates.choice(classes, data, itemSelectText);
const actualOutput = templates.choice(
choiceOptions,
data,
itemSelectText,
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -481,10 +518,10 @@ describe('templates', () => {
});
describe('input', () => {
const classes = {
const inputOptions = createOptionsWithPartialClasses({
input: 'class-1',
inputCloned: 'class-2',
};
});
it('returns expected html', () => {
/*
@ -496,52 +533,52 @@ describe('templates', () => {
<input
type="search"
name="search_terms"
class="${classes.input} ${classes.inputCloned}"
class="${inputOptions.classNames.input} ${inputOptions.classNames.inputCloned}"
autocomplete="off"
role="textbox"
aria-autocomplete="list"
aria-label="test placeholder"
>
`);
const actualOutput = templates.input(classes, 'test placeholder');
const actualOutput = templates.input(inputOptions, 'test placeholder');
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('dropdown', () => {
const classes = {
const dropdownOptions = createOptionsWithPartialClasses({
list: 'class-1',
listDropdown: 'class-2',
};
});
it('returns expected html', () => {
const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`,
`<div class="${dropdownOptions.classNames.list} ${dropdownOptions.classNames.listDropdown}" aria-expanded="false"></div>`,
);
const actualOutput = templates.dropdown(classes);
const actualOutput = templates.dropdown(dropdownOptions);
expectEqualElements(actualOutput, expectedOutput);
});
});
describe('notice', () => {
const classes = {
const noticeOptions = createOptionsWithPartialClasses({
item: 'class-1',
itemChoice: 'class-2',
noResults: 'class-3',
noChoices: 'class-4',
};
});
const label = 'test';
it('returns expected html', () => {
const expectedOutput = strToEl(`
<div class="${classes.item} ${classes.itemChoice}">
<div class="${noticeOptions.classNames.item} ${noticeOptions.classNames.itemChoice}">
${label}
</div>
`);
const actualOutput = templates.notice(classes, label);
const actualOutput = templates.notice(noticeOptions, label);
expectEqualElements(actualOutput, expectedOutput);
});
@ -550,11 +587,15 @@ describe('templates', () => {
describe('no results', () => {
it('adds no results classname', () => {
const expectedOutput = strToEl(`
<div class="${classes.item} ${classes.itemChoice} ${classes.noResults}">
<div class="${noticeOptions.classNames.item} ${noticeOptions.classNames.itemChoice} ${noticeOptions.classNames.noResults}">
${label}
</div>
`);
const actualOutput = templates.notice(classes, label, 'no-results');
const actualOutput = templates.notice(
noticeOptions,
label,
'no-results',
);
expectEqualElements(actualOutput, expectedOutput);
});
@ -563,11 +604,15 @@ describe('templates', () => {
describe('no choices', () => {
it('adds no choices classname', () => {
const expectedOutput = strToEl(`
<div class="${classes.item} ${classes.itemChoice} ${classes.noChoices}">
<div class="${noticeOptions.classNames.item} ${noticeOptions.classNames.itemChoice} ${noticeOptions.classNames.noChoices}">
${label}
</div>
`);
const actualOutput = templates.notice(classes, label, 'no-choices');
const actualOutput = templates.notice(
noticeOptions,
label,
'no-choices',
);
expectEqualElements(actualOutput, expectedOutput);
});

View File

@ -4,14 +4,16 @@
*/
import { Choice } from './interfaces/choice';
import { ClassNames } from './interfaces/class-names';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { PassedElementType } from './interfaces/passed-element-type';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TemplateOptions = Record<'classNames' | 'allowHTML', any>;
const templates = {
containerOuter(
{ containerOuter }: Pick<ClassNames, 'containerOuter'>,
{ classNames: { containerOuter } }: TemplateOptions,
dir: HTMLElement['dir'],
isSelectElement: boolean,
isSelectOneElement: boolean,
@ -46,19 +48,15 @@ const templates = {
},
containerInner({
containerInner,
}: Pick<ClassNames, 'containerInner'>): HTMLDivElement {
classNames: { containerInner },
}: TemplateOptions): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: containerInner,
});
},
itemList(
{
list,
listSingle,
listItems,
}: Pick<ClassNames, 'list' | 'listSingle' | 'listItems'>,
{ classNames: { list, listSingle, listItems } }: TemplateOptions,
isSelectOneElement: boolean,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
@ -67,26 +65,26 @@ const templates = {
},
placeholder(
{ placeholder }: Pick<ClassNames, 'placeholder'>,
{ allowHTML, classNames: { placeholder } }: TemplateOptions,
value: string,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: placeholder,
innerHTML: value,
[allowHTML ? 'innerHTML' : 'innerText']: value,
});
},
item(
{
item,
button,
highlightedState,
itemSelectable,
placeholder,
}: Pick<
ClassNames,
'item' | 'button' | 'highlightedState' | 'itemSelectable' | 'placeholder'
>,
allowHTML,
classNames: {
item,
button,
highlightedState,
itemSelectable,
placeholder,
},
}: TemplateOptions,
{
id,
value,
@ -101,7 +99,7 @@ const templates = {
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: item,
innerHTML: label,
[allowHTML ? 'innerHTML' : 'innerText']: label,
});
Object.assign(div.dataset, {
@ -135,7 +133,7 @@ const templates = {
const removeButton = Object.assign(document.createElement('button'), {
type: 'button',
className: button,
innerHTML: REMOVE_ITEM_TEXT,
[allowHTML ? 'innerHTML' : 'innerText']: REMOVE_ITEM_TEXT,
});
removeButton.setAttribute(
'aria-label',
@ -149,7 +147,7 @@ const templates = {
},
choiceList(
{ list }: Pick<ClassNames, 'list'>,
{ classNames: { list } }: TemplateOptions,
isSelectOneElement: boolean,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
@ -166,10 +164,9 @@ const templates = {
choiceGroup(
{
group,
groupHeading,
itemDisabled,
}: Pick<ClassNames, 'group' | 'groupHeading' | 'itemDisabled'>,
allowHTML,
classNames: { group, groupHeading, itemDisabled },
}: TemplateOptions,
{ id, value, disabled }: Group,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
@ -191,7 +188,7 @@ const templates = {
div.appendChild(
Object.assign(document.createElement('div'), {
className: groupHeading,
innerHTML: value,
[allowHTML ? 'innerHTML' : 'innerText']: value,
}),
);
@ -200,21 +197,16 @@ const templates = {
choice(
{
item,
itemChoice,
itemSelectable,
selectedState,
itemDisabled,
placeholder,
}: Pick<
ClassNames,
| 'item'
| 'itemChoice'
| 'itemSelectable'
| 'selectedState'
| 'itemDisabled'
| 'placeholder'
>,
allowHTML,
classNames: {
item,
itemChoice,
itemSelectable,
selectedState,
itemDisabled,
placeholder,
},
}: TemplateOptions,
{
id,
value,
@ -229,7 +221,7 @@ const templates = {
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
id: elementId,
innerHTML: label,
[allowHTML ? 'innerHTML' : 'innerText']: label,
className: `${item} ${itemChoice}`,
});
@ -263,7 +255,7 @@ const templates = {
},
input(
{ input, inputCloned }: Pick<ClassNames, 'input' | 'inputCloned'>,
{ classNames: { input, inputCloned } }: TemplateOptions,
placeholderValue: string,
): HTMLInputElement {
const inp = Object.assign(document.createElement('input'), {
@ -283,9 +275,8 @@ const templates = {
},
dropdown({
list,
listDropdown,
}: Pick<ClassNames, 'list' | 'listDropdown'>): HTMLDivElement {
classNames: { list, listDropdown },
}: TemplateOptions): HTMLDivElement {
const div = document.createElement('div');
div.classList.add(list, listDropdown);
@ -296,12 +287,10 @@ const templates = {
notice(
{
item,
itemChoice,
noResults,
noChoices,
}: Pick<ClassNames, 'item' | 'itemChoice' | 'noResults' | 'noChoices'>,
innerHTML: string,
allowHTML,
classNames: { item, itemChoice, noResults, noChoices },
}: TemplateOptions,
innerText: string,
type: 'no-choices' | 'no-results' | '' = '',
): HTMLDivElement {
const classes = [item, itemChoice];
@ -313,7 +302,7 @@ const templates = {
}
return Object.assign(document.createElement('div'), {
innerHTML,
[allowHTML ? 'innerHTML' : 'innerText']: innerText,
className: classes.join(' '),
});
},