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, removeItems: true,
removeItemButton: false, removeItemButton: false,
editItems: false, editItems: false,
allowHTML: true
duplicateItemsAllowed: true, duplicateItemsAllowed: true,
delimiter: ',', delimiter: ',',
paste: true, 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. **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 ### duplicateItemsAllowed
**Type:** `Boolean` **Default:** `true` **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 If you want just extend a little original template then you may use `Choices.defaults.templates` to get access to
original template function. 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:** **Example:**
```js ```js
@ -656,7 +669,7 @@ or more complex:
const example = new Choices(element, { const example = new Choices(element, {
callbackOnCreateTemplates: function(template) { callbackOnCreateTemplates: function(template) {
return { return {
item: (classNames, data) => { item: ({ classNames }, data) => {
return template(` return template(`
<div class="${classNames.item} ${ <div class="${classNames.item} ${
data.highlighted data.highlighted
@ -671,7 +684,7 @@ const example = new Choices(element, {
</div> </div>
`); `);
}, },
choice: (classNames, data) => { choice: ({ classNames }, data) => {
return template(` return template(`
<div class="${classNames.item} ${classNames.itemChoice} ${ <div class="${classNames.item} ${classNames.itemChoice} ${
data.disabled ? classNames.itemDisabled : classNames.itemSelectable data.disabled ? classNames.itemDisabled : classNames.itemSelectable

View file

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

View file

@ -1,6 +1,10 @@
describe('Choices - text element', () => { describe('Choices - text element', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/text'); cy.visit('/text', {
onBeforeLoad(win) {
cy.stub(win.console, 'warn').as('consoleWarn');
},
});
}); });
describe('scenarios', () => { describe('scenarios', () => {
@ -17,7 +21,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=basic]') cy.get('[data-test-hook=basic]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($el => { .should(($el) => {
expect($el).to.contain(textInput); expect($el).to.contain(textInput);
}); });
}); });
@ -42,7 +46,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=basic]') cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown') .find('.choices__list--dropdown')
.should('be.visible') .should('be.visible')
.should($dropdown => { .should(($dropdown) => {
const dropdownText = $dropdown.text().trim(); const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal( expect(dropdownText).to.equal(
`Press Enter to add "${textInput}"`, `Press Enter to add "${textInput}"`,
@ -74,7 +78,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=edit-items]') cy.get('[data-test-hook=edit-items]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($choice => { .should(($choice) => {
expect($choice.data('value')).to.equal(`${textInput}-edited`); expect($choice.data('value')).to.equal(`${textInput}-edited`);
}); });
}); });
@ -90,7 +94,7 @@ describe('Choices - text element', () => {
it('highlights all items', () => { it('highlights all items', () => {
cy.get('[data-test-hook=edit-items]') cy.get('[data-test-hook=edit-items]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.each($choice => { .each(($choice) => {
expect($choice.hasClass('is-highlighted')).to.equal(true); expect($choice.hasClass('is-highlighted')).to.equal(true);
}); });
}); });
@ -124,7 +128,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]') cy.get('[data-test-hook=remove-button]')
.find('.choices__list--multiple') .find('.choices__list--multiple')
.children() .children()
.should($items => { .should(($items) => {
expect($items.length).to.equal(1); expect($items.length).to.equal(1);
}); });
@ -137,7 +141,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]') cy.get('[data-test-hook=remove-button]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.should($items => { .should(($items) => {
expect($items.length).to.equal(0); expect($items.length).to.equal(0);
}); });
}); });
@ -152,7 +156,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=remove-button]') cy.get('[data-test-hook=remove-button]')
.find('.choices__input[hidden]') .find('.choices__input[hidden]')
.then($input => { .then(($input) => {
expect($input.val()).to.not.contain(textInput); expect($input.val()).to.not.contain(textInput);
}); });
}); });
@ -175,7 +179,7 @@ describe('Choices - text element', () => {
.find('.choices__list--multiple') .find('.choices__list--multiple')
.first() .first()
.children() .children()
.should($items => { .should(($items) => {
expect($items.length).to.equal(1); expect($items.length).to.equal(1);
}); });
}); });
@ -185,7 +189,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=unique-values]') cy.get('[data-test-hook=unique-values]')
.find('.choices__list--dropdown') .find('.choices__list--dropdown')
.should('be.visible') .should('be.visible')
.should($dropdown => { .should(($dropdown) => {
const dropdownText = $dropdown.text().trim(); const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal( expect(dropdownText).to.equal(
'Only unique values can be added', 'Only unique values can be added',
@ -212,7 +216,7 @@ describe('Choices - text element', () => {
.find('.choices__list--multiple') .find('.choices__list--multiple')
.first() .first()
.children() .children()
.should($items => { .should(($items) => {
expect($items.length).to.equal(inputLimit); expect($items.length).to.equal(inputLimit);
}); });
}); });
@ -222,7 +226,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=input-limit]') cy.get('[data-test-hook=input-limit]')
.find('.choices__list--dropdown') .find('.choices__list--dropdown')
.should('be.visible') .should('be.visible')
.should($dropdown => { .should(($dropdown) => {
const dropdownText = $dropdown.text().trim(); const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal( expect(dropdownText).to.equal(
`Only ${inputLimit} values can be added`, `Only ${inputLimit} values can be added`,
@ -245,7 +249,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=add-item-filter]') cy.get('[data-test-hook=add-item-filter]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($choice => { .should(($choice) => {
expect($choice.text().trim()).to.equal(input); expect($choice.text().trim()).to.equal(input);
}); });
}); });
@ -261,7 +265,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=add-item-filter]') cy.get('[data-test-hook=add-item-filter]')
.find('.choices__list--dropdown') .find('.choices__list--dropdown')
.should('be.visible') .should('be.visible')
.should($dropdown => { .should(($dropdown) => {
const dropdownText = $dropdown.text().trim(); const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal( expect(dropdownText).to.equal(
'Only values matching specific conditions can be added', 'Only values matching specific conditions can be added',
@ -283,7 +287,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=prepend-append]') cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($choice => { .should(($choice) => {
expect($choice.data('value')).to.equal(`before-${textInput}-after`); expect($choice.data('value')).to.equal(`before-${textInput}-after`);
}); });
}); });
@ -292,7 +296,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=prepend-append]') cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($choice => { .should(($choice) => {
expect($choice.text()).to.not.contain(`before-${textInput}-after`); expect($choice.text()).to.not.contain(`before-${textInput}-after`);
expect($choice.text()).to.contain(textInput); expect($choice.text()).to.contain(textInput);
}); });
@ -319,21 +323,21 @@ describe('Choices - text element', () => {
it('pre-populates choices', () => { it('pre-populates choices', () => {
cy.get('[data-test-hook=prepopulated]') cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.should($choices => { .should(($choices) => {
expect($choices.length).to.equal(2); expect($choices.length).to.equal(2);
}); });
cy.get('[data-test-hook=prepopulated]') cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.first() .first()
.should($choice => { .should(($choice) => {
expect($choice.text().trim()).to.equal('Josh Johnson'); expect($choice.text().trim()).to.equal('Josh Johnson');
}); });
cy.get('[data-test-hook=prepopulated]') cy.get('[data-test-hook=prepopulated]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($choice => { .should(($choice) => {
expect($choice.text().trim()).to.equal('Joe Bloggs'); 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('within form', () => {
describe('inputting item', () => { describe('inputting item', () => {
describe('on enter key', () => { describe('on enter key', () => {
it('does not submit form', () => { 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(() => { $form.submit(() => {
// this will fail the test if the form submits // this will fail the test if the form submits
throw new Error('Form submitted'); throw new Error('Form submitted');
@ -374,7 +420,7 @@ describe('Choices - text element', () => {
cy.get('[data-test-hook=within-form]') cy.get('[data-test-hook=within-form]')
.find('.choices__list--multiple .choices__item') .find('.choices__list--multiple .choices__item')
.last() .last()
.should($el => { .should(($el) => {
expect($el).to.contain(textInput); 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) { (function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object') if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(); module.exports = factory();
@ -291,6 +291,10 @@ function () {
userConfig = {}; 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, 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 // instead of concatenating with the default array
{ {
@ -2209,8 +2213,7 @@ function () {
args[_i - 1] = arguments[_i]; args[_i - 1] = arguments[_i];
} }
var classNames = this.config.classNames; return (_a = this._templates[template]).call.apply(_a, __spreadArray([this, this.config], args, false));
return (_a = this._templates[template]).call.apply(_a, __spreadArray([this, classNames], args, false));
}; };
Choices.prototype._createTemplates = function () { Choices.prototype._createTemplates = function () {
@ -3483,6 +3486,7 @@ exports.DEFAULT_CONFIG = {
removeItems: true, removeItems: true,
removeItemButton: false, removeItemButton: false,
editItems: false, editItems: false,
allowHTML: true,
duplicateItemsAllowed: true, duplicateItemsAllowed: true,
delimiter: ',', delimiter: ',',
paste: true, paste: true,
@ -3644,7 +3648,7 @@ var sanitise = function (value) {
return 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; exports.sanitise = sanitise;
@ -4376,7 +4380,7 @@ Object.defineProperty(exports, "__esModule", ({
})); }));
var templates = { var templates = {
containerOuter: function (_a, dir, isSelectElement, isSelectOneElement, searchEnabled, passedElementType) { containerOuter: function (_a, dir, isSelectElement, isSelectOneElement, searchEnabled, passedElementType) {
var containerOuter = _a.containerOuter; var containerOuter = _a.classNames.containerOuter;
var div = Object.assign(document.createElement('div'), { var div = Object.assign(document.createElement('div'), {
className: containerOuter className: containerOuter
}); });
@ -4403,32 +4407,39 @@ var templates = {
return div; return div;
}, },
containerInner: function (_a) { containerInner: function (_a) {
var containerInner = _a.containerInner; var containerInner = _a.classNames.containerInner;
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: containerInner className: containerInner
}); });
}, },
itemList: function (_a, isSelectOneElement) { itemList: function (_a, isSelectOneElement) {
var list = _a.list, var _b = _a.classNames,
listSingle = _a.listSingle, list = _b.list,
listItems = _a.listItems; listSingle = _b.listSingle,
listItems = _b.listItems;
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: "".concat(list, " ").concat(isSelectOneElement ? listSingle : listItems) className: "".concat(list, " ").concat(isSelectOneElement ? listSingle : listItems)
}); });
}, },
placeholder: function (_a, value) { placeholder: function (_a, value) {
var placeholder = _a.placeholder; var _b;
return Object.assign(document.createElement('div'), {
className: placeholder, var allowHTML = _a.allowHTML,
innerHTML: value placeholder = _a.classNames.placeholder;
}); return Object.assign(document.createElement('div'), (_b = {
className: placeholder
}, _b[allowHTML ? 'innerHTML' : 'innerText'] = value, _b));
}, },
item: function (_a, _b, removeItemButton) { item: function (_a, _b, removeItemButton) {
var item = _a.item, var _c, _d;
button = _a.button,
highlightedState = _a.highlightedState, var allowHTML = _a.allowHTML,
itemSelectable = _a.itemSelectable, _e = _a.classNames,
placeholder = _a.placeholder; item = _e.item,
button = _e.button,
highlightedState = _e.highlightedState,
itemSelectable = _e.itemSelectable,
placeholder = _e.placeholder;
var id = _b.id, var id = _b.id,
value = _b.value, value = _b.value,
label = _b.label, label = _b.label,
@ -4437,10 +4448,9 @@ var templates = {
disabled = _b.disabled, disabled = _b.disabled,
highlighted = _b.highlighted, highlighted = _b.highlighted,
isPlaceholder = _b.placeholder; isPlaceholder = _b.placeholder;
var div = Object.assign(document.createElement('div'), { var div = Object.assign(document.createElement('div'), (_c = {
className: item, className: item
innerHTML: label }, _c[allowHTML ? 'innerHTML' : 'innerText'] = label, _c));
});
Object.assign(div.dataset, { Object.assign(div.dataset, {
item: '', item: '',
id: id, id: id,
@ -4471,11 +4481,10 @@ var templates = {
/** @todo This MUST be localizable, not hardcoded! */ /** @todo This MUST be localizable, not hardcoded! */
var REMOVE_ITEM_TEXT = 'Remove item'; var REMOVE_ITEM_TEXT = 'Remove item';
var removeButton = Object.assign(document.createElement('button'), { var removeButton = Object.assign(document.createElement('button'), (_d = {
type: 'button', type: 'button',
className: button, className: button
innerHTML: REMOVE_ITEM_TEXT }, _d[allowHTML ? 'innerHTML' : 'innerText'] = REMOVE_ITEM_TEXT, _d));
});
removeButton.setAttribute('aria-label', "".concat(REMOVE_ITEM_TEXT, ": '").concat(value, "'")); removeButton.setAttribute('aria-label', "".concat(REMOVE_ITEM_TEXT, ": '").concat(value, "'"));
removeButton.dataset.button = ''; removeButton.dataset.button = '';
div.appendChild(removeButton); div.appendChild(removeButton);
@ -4484,7 +4493,7 @@ var templates = {
return div; return div;
}, },
choiceList: function (_a, isSelectOneElement) { choiceList: function (_a, isSelectOneElement) {
var list = _a.list; var list = _a.classNames.list;
var div = Object.assign(document.createElement('div'), { var div = Object.assign(document.createElement('div'), {
className: list className: list
}); });
@ -4497,9 +4506,13 @@ var templates = {
return div; return div;
}, },
choiceGroup: function (_a, _b) { choiceGroup: function (_a, _b) {
var group = _a.group, var _c;
groupHeading = _a.groupHeading,
itemDisabled = _a.itemDisabled; var allowHTML = _a.allowHTML,
_d = _a.classNames,
group = _d.group,
groupHeading = _d.groupHeading,
itemDisabled = _d.itemDisabled;
var id = _b.id, var id = _b.id,
value = _b.value, value = _b.value,
disabled = _b.disabled; disabled = _b.disabled;
@ -4517,19 +4530,22 @@ var templates = {
div.setAttribute('aria-disabled', 'true'); div.setAttribute('aria-disabled', 'true');
} }
div.appendChild(Object.assign(document.createElement('div'), { div.appendChild(Object.assign(document.createElement('div'), (_c = {
className: groupHeading, className: groupHeading
innerHTML: value }, _c[allowHTML ? 'innerHTML' : 'innerText'] = value, _c)));
}));
return div; return div;
}, },
choice: function (_a, _b, selectText) { choice: function (_a, _b, selectText) {
var item = _a.item, var _c;
itemChoice = _a.itemChoice,
itemSelectable = _a.itemSelectable, var allowHTML = _a.allowHTML,
selectedState = _a.selectedState, _d = _a.classNames,
itemDisabled = _a.itemDisabled, item = _d.item,
placeholder = _a.placeholder; itemChoice = _d.itemChoice,
itemSelectable = _d.itemSelectable,
selectedState = _d.selectedState,
itemDisabled = _d.itemDisabled,
placeholder = _d.placeholder;
var id = _b.id, var id = _b.id,
value = _b.value, value = _b.value,
label = _b.label, label = _b.label,
@ -4538,11 +4554,9 @@ var templates = {
isDisabled = _b.disabled, isDisabled = _b.disabled,
isSelected = _b.selected, isSelected = _b.selected,
isPlaceholder = _b.placeholder; isPlaceholder = _b.placeholder;
var div = Object.assign(document.createElement('div'), { var div = Object.assign(document.createElement('div'), (_c = {
id: elementId, id: elementId
innerHTML: label, }, _c[allowHTML ? 'innerHTML' : 'innerText'] = label, _c.className = "".concat(item, " ").concat(itemChoice), _c));
className: "".concat(item, " ").concat(itemChoice)
});
if (isSelected) { if (isSelected) {
div.classList.add(selectedState); div.classList.add(selectedState);
@ -4572,10 +4586,12 @@ var templates = {
return div; return div;
}, },
input: function (_a, placeholderValue) { input: function (_a, placeholderValue) {
var input = _a.input, var _b = _a.classNames,
inputCloned = _a.inputCloned; input = _b.input,
inputCloned = _b.inputCloned;
var inp = Object.assign(document.createElement('input'), { var inp = Object.assign(document.createElement('input'), {
type: 'text', type: 'search',
name: 'search_terms',
className: "".concat(input, " ").concat(inputCloned), className: "".concat(input, " ").concat(inputCloned),
autocomplete: 'off', autocomplete: 'off',
autocapitalize: 'off', autocapitalize: 'off',
@ -4587,18 +4603,23 @@ var templates = {
return inp; return inp;
}, },
dropdown: function (_a) { dropdown: function (_a) {
var list = _a.list, var _b = _a.classNames,
listDropdown = _a.listDropdown; list = _b.list,
listDropdown = _b.listDropdown;
var div = document.createElement('div'); var div = document.createElement('div');
div.classList.add(list, listDropdown); div.classList.add(list, listDropdown);
div.setAttribute('aria-expanded', 'false'); div.setAttribute('aria-expanded', 'false');
return div; return div;
}, },
notice: function (_a, innerHTML, type) { notice: function (_a, innerText, type) {
var item = _a.item, var _b;
itemChoice = _a.itemChoice,
noResults = _a.noResults, var allowHTML = _a.allowHTML,
noChoices = _a.noChoices; _c = _a.classNames,
item = _c.item,
itemChoice = _c.itemChoice,
noResults = _c.noResults,
noChoices = _c.noChoices;
if (type === void 0) { if (type === void 0) {
type = ''; type = '';
@ -4612,10 +4633,7 @@ var templates = {
classes.push(noResults); classes.push(noResults);
} }
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), (_b = {}, _b[allowHTML ? 'innerHTML' : 'innerText'] = innerText, _b.className = classes.join(' '), _b));
innerHTML: innerHTML,
className: classes.join(' ')
});
}, },
option: function (_a) { option: function (_a) {
var label = _a.label, 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) { for (i = 0; i < genericExamples.length; ++i) {
var element = genericExamples[i]; var element = genericExamples[i];
new Choices(element, { new Choices(element, {
allowHTML: true,
placeholderValue: 'This is a placeholder set in the config', placeholderValue: 'This is a placeholder set in the config',
searchPlaceholderValue: 'This is a search placeholder', searchPlaceholderValue: 'This is a search placeholder',
}); });
@ -542,6 +543,7 @@
var textRemove = new Choices( var textRemove = new Choices(
document.getElementById('choices-text-remove-button'), document.getElementById('choices-text-remove-button'),
{ {
allowHTML: true,
delimiter: ',', delimiter: ',',
editItems: true, editItems: true,
maxItemCount: 5, maxItemCount: 5,
@ -550,12 +552,14 @@
); );
var textUniqueVals = new Choices('#choices-text-unique-values', { var textUniqueVals = new Choices('#choices-text-unique-values', {
allowHTML: true,
paste: false, paste: false,
duplicateItemsAllowed: false, duplicateItemsAllowed: false,
editItems: true, editItems: true,
}); });
var texti18n = new Choices('#choices-text-i18n', { var texti18n = new Choices('#choices-text-i18n', {
allowHTML: true,
paste: false, paste: false,
duplicateItemsAllowed: false, duplicateItemsAllowed: false,
editItems: true, editItems: true,
@ -572,6 +576,7 @@
}); });
var textEmailFilter = new Choices('#choices-text-email-filter', { var textEmailFilter = new Choices('#choices-text-email-filter', {
allowHTML: true,
editItems: true, editItems: true,
addItemFilter: function(value) { addItemFilter: function(value) {
if (!value) { if (!value) {
@ -585,6 +590,7 @@
}).setValue(['joe@bloggs.com']); }).setValue(['joe@bloggs.com']);
var textDisabled = new Choices('#choices-text-disabled', { var textDisabled = new Choices('#choices-text-disabled', {
allowHTML: true,
addItems: false, addItems: false,
removeItems: false, removeItems: false,
}).disable(); }).disable();
@ -592,12 +598,14 @@
var textPrependAppendVal = new Choices( var textPrependAppendVal = new Choices(
'#choices-text-prepend-append-value', '#choices-text-prepend-append-value',
{ {
allowHTML: true,
prependValue: 'item-', prependValue: 'item-',
appendValue: '-' + Date.now(), appendValue: '-' + Date.now(),
} }
).removeActiveItems(); ).removeActiveItems();
var textPresetVal = new Choices('#choices-text-preset-values', { var textPresetVal = new Choices('#choices-text-preset-values', {
allowHTML: true,
items: [ items: [
'Josh Johnson', 'Josh Johnson',
{ {
@ -611,10 +619,12 @@
}); });
var multipleDefault = new Choices( var multipleDefault = new Choices(
document.getElementById('choices-multiple-groups') document.getElementById('choices-multiple-groups'),
{ allowHTML: true }
); );
var multipleFetch = new Choices('#choices-multiple-remote-fetch', { var multipleFetch = new Choices('#choices-multiple-remote-fetch', {
allowHTML: false,
placeholder: true, placeholder: true,
placeholderValue: 'Pick an Strokes record', placeholderValue: 'Pick an Strokes record',
maxItemCount: 5, maxItemCount: 5,
@ -635,12 +645,14 @@
var multipleCancelButton = new Choices( var multipleCancelButton = new Choices(
'#choices-multiple-remove-button', '#choices-multiple-remove-button',
{ {
allowHTML: true,
removeItemButton: true, removeItemButton: true,
} }
); );
/* Use label on event */ /* Use label on event */
var choicesSelect = new Choices('#choices-multiple-labels', { var choicesSelect = new Choices('#choices-multiple-labels', {
allowHTML: true,
removeItemButton: true, removeItemButton: true,
choices: [ choices: [
{ value: 'One', label: 'Label One' }, { value: 'One', label: 'Label One' },
@ -675,6 +687,7 @@
); );
var singleFetch = new Choices('#choices-single-remote-fetch', { var singleFetch = new Choices('#choices-single-remote-fetch', {
allowHTML: false,
searchPlaceholderValue: 'Search for an Arctic Monkeys record', searchPlaceholderValue: 'Search for an Arctic Monkeys record',
}) })
.setChoices(function() { .setChoices(function() {
@ -695,6 +708,7 @@
}); });
var singleXhrRemove = new Choices('#choices-single-remove-xhr', { var singleXhrRemove = new Choices('#choices-single-remove-xhr', {
allowHTML: true,
removeItemButton: true, removeItemButton: true,
searchPlaceholderValue: "Search for a Smiths' record", searchPlaceholderValue: "Search for a Smiths' record",
}).setChoices(function(callback) { }).setChoices(function(callback) {
@ -712,6 +726,7 @@
}); });
var singleNoSearch = new Choices('#choices-single-no-search', { var singleNoSearch = new Choices('#choices-single-no-search', {
allowHTML: true,
searchEnabled: false, searchEnabled: false,
removeItemButton: true, removeItemButton: true,
choices: [ choices: [
@ -731,6 +746,7 @@
); );
var singlePresetOpts = new Choices('#choices-single-preset-options', { var singlePresetOpts = new Choices('#choices-single-preset-options', {
allowHTML: true,
placeholder: true, placeholder: true,
}).setChoices( }).setChoices(
[ [
@ -760,6 +776,7 @@
); );
var singleSelectedOpt = new Choices('#choices-single-selected-option', { var singleSelectedOpt = new Choices('#choices-single-selected-option', {
allowHTML: true,
searchFields: ['label', 'value', 'customProperties.description'], searchFields: ['label', 'value', 'customProperties.description'],
choices: [ choices: [
{ value: 'One', label: 'Label One', selected: true }, { value: 'One', label: 'Label One', selected: true },
@ -777,17 +794,20 @@
var customChoicesPropertiesViaDataAttributes = new Choices( var customChoicesPropertiesViaDataAttributes = new Choices(
'#choices-with-custom-props-via-html', '#choices-with-custom-props-via-html',
{ {
allowHTML: true,
searchFields: ['label', 'value', 'customProperties'], searchFields: ['label', 'value', 'customProperties'],
} }
); );
var singleNoSorting = new Choices('#choices-single-no-sorting', { var singleNoSorting = new Choices('#choices-single-no-sorting', {
allowHTML: true,
shouldSort: false, shouldSort: false,
}); });
var cities = new Choices(document.getElementById('cities')); var cities = new Choices(document.getElementById('cities'), { allowHTML: true });
var tubeStations = new Choices( var tubeStations = new Choices(
document.getElementById('tube-stations') document.getElementById('tube-stations'),
{ allowHTML: true }
).disable(); ).disable();
cities.passedElement.element.addEventListener('change', function(e) { cities.passedElement.element.addEventListener('change', function(e) {
@ -801,11 +821,12 @@
var customTemplates = new Choices( var customTemplates = new Choices(
document.getElementById('choices-single-custom-templates'), document.getElementById('choices-single-custom-templates'),
{ {
allowHTML: true,
callbackOnCreateTemplates: function(strToEl) { callbackOnCreateTemplates: function(strToEl) {
var classNames = this.config.classNames; var classNames = this.config.classNames;
var itemSelectText = this.config.itemSelectText; var itemSelectText = this.config.itemSelectText;
return { return {
item: function(classNames, data) { item: function({ classNames }, data) {
return strToEl( return strToEl(
'\ '\
<div\ <div\
@ -839,7 +860,7 @@
' '
); );
}, },
choice: function(classNames, data) { choice: function({ classNames }, data) {
return strToEl( return strToEl(
'\ '\
<div\ <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', { var resetMultiple = new Choices('#reset-multiple', {
allowHTML: true,
removeItemButton: true, removeItemButton: true,
}); });
}); });

View file

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

View file

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

View file

@ -76,6 +76,21 @@
<input class="form-control" id="choices-unique-values" type="text" /> <input class="form-control" id="choices-unique-values" type="text" />
</div> </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"> <div data-test-hook="input-limit">
<label for="choices-input-limit">Input limit</label> <label for="choices-input-limit">Input limit</label>
<input class="form-control" id="choices-input-limit" type="text" /> <input class="form-control" id="choices-input-limit" type="text" />
@ -134,25 +149,52 @@
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
new Choices('#choices-basic'); new Choices('#choices-basic', {
allowHTML: true,
});
new Choices('#choices-edit-items', { new Choices('#choices-edit-items', {
allowHTML: true,
editItems: true, editItems: true,
}); });
new Choices('#choices-remove-button', { new Choices('#choices-remove-button', {
allowHTML: true,
removeItemButton: true, removeItemButton: true,
}); });
new Choices('#choices-unique-values', { new Choices('#choices-unique-values', {
allowHTML: true,
duplicateItemsAllowed: false, 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', { new Choices('#choices-input-limit', {
allowHTML: true,
maxItemCount: 5, maxItemCount: 5,
}); });
new Choices('#choices-add-item-filter', { new Choices('#choices-add-item-filter', {
allowHTML: true,
addItems: true, addItems: true,
addItemFilter: value => { 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,}))$/; 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', { new Choices('#choices-adding-items-disabled', {
allowHTML: true,
addItems: false, addItems: false,
}); });
new Choices('#choices-disabled-via-attr'); new Choices('#choices-disabled-via-attr', {
allowHTML: true,
});
new Choices('#choices-prepend-append', { new Choices('#choices-prepend-append', {
allowHTML: true,
prependValue: 'before-', prependValue: 'before-',
appendValue: '-after', appendValue: '-after',
}); });
new Choices('#choices-prepopulated', { new Choices('#choices-prepopulated', {
allowHTML: true,
items: [ items: [
'Josh Johnson', 'Josh Johnson',
{ {
@ -186,11 +233,14 @@
}); });
new Choices('#choices-placeholder', { new Choices('#choices-placeholder', {
allowHTML: true,
placeholder: true, placeholder: true,
placeholderValue: 'I am a placeholder', placeholderValue: 'I am a placeholder',
}); });
new Choices('#choices-within-form'); new Choices('#choices-within-form', {
allowHTML: true,
});
}); });
</script> </script>
</body> </body>

View file

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

View file

@ -151,6 +151,12 @@ class Choices implements Choices {
| HTMLSelectElement = '[data-choice]', | HTMLSelectElement = '[data-choice]',
userConfig: Partial<Options> = {}, 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>( this.config = merge.all<Options>(
[DEFAULT_CONFIG, Choices.defaults.options, userConfig], [DEFAULT_CONFIG, Choices.defaults.options, userConfig],
// When merging array configs, replace with a copy of the userConfig array, // 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 { _getTemplate(template: string, ...args: any): any {
const { classNames } = this.config; return this._templates[template].call(this, this.config, ...args);
return this._templates[template].call(this, classNames, ...args);
} }
_createTemplates(): void { _createTemplates(): void {

View file

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

View file

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

View file

@ -159,6 +159,19 @@ export interface Options {
*/ */
editItems: boolean; 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. * Whether each inputted/chosen item should be unique.
* *

View file

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

View file

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