feat: allowHTML

This commit is contained in:
viction 2021-12-23 16:59:48 +00:00
parent 82b94228f9
commit 7f727480e8
6 changed files with 64 additions and 46 deletions

View file

@ -109,6 +109,7 @@ Or include Choices directly:
removeItems: true,
removeItemButton: false,
editItems: false,
allowHTML: true
duplicateItemsAllowed: true,
delimiter: ',',
paste: true,
@ -314,6 +315,14 @@ Pass an array of objects:
**Usage:** Whether a user can edit items. An item's value can be edited by pressing the backspace.
### allowHTML
**Type:** `Boolean` **Default:** `true`
**Input types affected:** `text`, `select-one`, `select-multiple
**Usage:** Whether HTML should be shown properly when showing choices. (Can be used to perform XSS attacks if not disabled or handled correctly)
### duplicateItemsAllowed
**Type:** `Boolean` **Default:** `true`

View file

@ -151,6 +151,10 @@ class Choices implements Choices {
| HTMLSelectElement = '[data-choice]',
userConfig: Partial<Options> = {},
) {
if (userConfig.allowHTML === undefined) {
console.warn('Deprecation warning: allowHTML in the future will be defaulted to false. You must explicitly set it to true to properly display html tags in choices.');
}
this.config = merge.all<Options>(
[DEFAULT_CONFIG, Choices.defaults.options, userConfig],
// When merging array configs, replace with a copy of the userConfig array,
@ -2105,9 +2109,7 @@ class Choices implements Choices {
}
_getTemplate(template: string, ...args: any): any {
const { classNames } = this.config;
return this._templates[template].call(this, classNames, ...args);
return this._templates[template].call(this, this.config, ...args);
}
_createTemplates(): void {

View file

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

View file

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

View file

@ -159,6 +159,16 @@ export interface Options {
*/
editItems: boolean;
/**
* Whether HTML should be shown properly when showing choices.
* (Can be used to perform XSS attacks if not disabled or handled correctly)
*
* **Input types affected:** text, select-one, select-multiple
*
* @default true
*/
allowHTML: boolean;
/**
* Whether each inputted/chosen item should be unique.
*

View file

@ -4,14 +4,14 @@
*/
import { Choice } from './interfaces/choice';
import { ClassNames } from './interfaces/class-names';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { Options } from './interfaces/options';
import { PassedElementType } from './interfaces/passed-element-type';
const templates = {
containerOuter(
{ containerOuter }: Pick<ClassNames, 'containerOuter'>,
{ classNames: { containerOuter } }: Options,
dir: HTMLElement['dir'],
isSelectElement: boolean,
isSelectOneElement: boolean,
@ -46,19 +46,19 @@ const templates = {
},
containerInner({
containerInner,
}: Pick<ClassNames, 'containerInner'>): HTMLDivElement {
classNames: { containerInner },
}: Options): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: containerInner,
});
},
itemList(
{
itemList({
classNames: {
list,
listSingle,
listItems,
}: Pick<ClassNames, 'list' | 'listSingle' | 'listItems'>,
}}: Options,
isSelectOneElement: boolean,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
@ -67,26 +67,22 @@ const templates = {
},
placeholder(
{ placeholder }: Pick<ClassNames, 'placeholder'>,
{ allowHTML, classNames: { placeholder } }: Options,
value: string,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: placeholder,
innerHTML: value,
[allowHTML ? 'innerHTML' : 'innerText']: value,
});
},
item(
{
item({ allowHTML, classNames: {
item,
button,
highlightedState,
itemSelectable,
placeholder,
}: Pick<
ClassNames,
'item' | 'button' | 'highlightedState' | 'itemSelectable' | 'placeholder'
>,
} }: Options,
{
id,
value,
@ -101,7 +97,7 @@ const templates = {
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: item,
innerHTML: label,
[allowHTML ? 'innerHTML' : 'innerText']: label,
});
Object.assign(div.dataset, {
@ -135,7 +131,7 @@ const templates = {
const removeButton = Object.assign(document.createElement('button'), {
type: 'button',
className: button,
innerHTML: REMOVE_ITEM_TEXT,
[allowHTML ? 'innerHTML' : 'innerText']: REMOVE_ITEM_TEXT,
});
removeButton.setAttribute(
'aria-label',
@ -149,7 +145,7 @@ const templates = {
},
choiceList(
{ list }: Pick<ClassNames, 'list'>,
{ classNames: { list } }: Options,
isSelectOneElement: boolean,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
@ -164,12 +160,13 @@ const templates = {
return div;
},
choiceGroup(
{
choiceGroup({
allowHTML,
classNames: {
group,
groupHeading,
itemDisabled,
}: Pick<ClassNames, 'group' | 'groupHeading' | 'itemDisabled'>,
} }: Options,
{ id, value, disabled }: Group,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
@ -191,30 +188,24 @@ const templates = {
div.appendChild(
Object.assign(document.createElement('div'), {
className: groupHeading,
innerHTML: value,
[allowHTML ? 'innerHTML' : 'innerText']: value,
}),
);
return div;
},
choice(
{
choice({
allowHTML,
classNames: {
item,
itemChoice,
itemSelectable,
selectedState,
itemDisabled,
placeholder,
}: Pick<
ClassNames,
| 'item'
| 'itemChoice'
| 'itemSelectable'
| 'selectedState'
| 'itemDisabled'
| 'placeholder'
>,
}
}: Options,
{
id,
value,
@ -229,7 +220,7 @@ const templates = {
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
id: elementId,
innerHTML: label,
[allowHTML ? 'innerHTML' : 'innerText']: label,
className: `${item} ${itemChoice}`,
});
@ -263,7 +254,7 @@ const templates = {
},
input(
{ input, inputCloned }: Pick<ClassNames, 'input' | 'inputCloned'>,
{ classNames: { input, inputCloned } }: Options,
placeholderValue: string,
): HTMLInputElement {
const inp = Object.assign(document.createElement('input'), {
@ -283,9 +274,11 @@ const templates = {
},
dropdown({
list,
listDropdown,
}: Pick<ClassNames, 'list' | 'listDropdown'>): HTMLDivElement {
classNames: {
list,
listDropdown,
}
}: Options): HTMLDivElement {
const div = document.createElement('div');
div.classList.add(list, listDropdown);
@ -294,14 +287,16 @@ const templates = {
return div;
},
notice(
{
notice({
allowHTML,
classNames: {
item,
itemChoice,
noResults,
noChoices,
}: Pick<ClassNames, 'item' | 'itemChoice' | 'noResults' | 'noChoices'>,
innerHTML: string,
}
}: Options,
innerText: string,
type: 'no-choices' | 'no-results' | '' = '',
): HTMLDivElement {
const classes = [item, itemChoice];
@ -313,7 +308,7 @@ const templates = {
}
return Object.assign(document.createElement('div'), {
innerHTML,
[allowHTML ? 'innerHTML' : 'innerText']: innerText,
className: classes.join(' '),
});
},