Skip to content

Commit 50f4652

Browse files
fix: data collection shouldn't render label as html by default (#86)
- when using `data` option to provide a collection of data, we should only render when `renderOptionLabelAsHtml` or `useSelectOptionLabelToHtml` is enabled, otherwise we should display same text input (html encoded instead of being rendered as html) Co-authored-by: ghiscoding <[email protected]>
1 parent a1bc0c8 commit 50f4652

File tree

5 files changed

+123
-17
lines changed

5 files changed

+123
-17
lines changed

demo/src/options/options27.html

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@ <h2 class="bd-title">
2626

2727
<div>
2828
<div class="mb-3 row">
29-
<label class="col-sm-2"> Basic Select </label>
29+
<label class="col-sm-4">Enable/Disable <code>renderOptionLabelAsHtml</code> option</label>
3030

31-
<div class="col-sm-10">
32-
<select multiple="multiple" class="full-width">
31+
<div class="col-sm-8">
32+
<button id="enableRenderHtml" class="btn btn-primary">Enable renderOptionLabelAsHtml</button>
33+
<button id="disableRenderHtml" class="btn btn-secondary">Disable renderOptionLabelAsHtml</button>
34+
</div>
35+
</div>
36+
37+
<div class="mb-3 row">
38+
<label class="col-sm-4">Basic Select</label>
39+
40+
<div class="col-sm-8">
41+
<select id="basic" data-test="select1" multiple="multiple" class="full-width">
3342
<option value="1">January</option>
3443
<option value="2">February</option>
3544
<option value="3">March</option>
@@ -45,4 +54,13 @@ <h2 class="bd-title">
4554
</select>
4655
</div>
4756
</div>
57+
58+
<div class="mb-3 row">
59+
<label class="col-sm-4">From Data</label>
60+
61+
<div class="col-sm-8">
62+
<select id="from-data" data-test="select2" class="full-width" multiple></select>
63+
</select>
64+
</div>
65+
</div>
4866
</div>

demo/src/options/options27.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { multipleSelect, MultipleSelectInstance, TextFilter } from 'multiple-sel
22

33
export default class Example {
44
ms1?: MultipleSelectInstance;
5+
ms2?: MultipleSelectInstance;
6+
btnEnableElm?: HTMLButtonElement | null;
7+
btnDisableElm?: HTMLButtonElement | null;
58

69
mount() {
7-
this.ms1 = multipleSelect('select', {
10+
this.ms1 = multipleSelect('#basic', {
811
filter: true,
12+
displayTitle: true,
913
renderOptionLabelAsHtml: true, // without this flag, html code will be showing as plain text
1014
textTemplate: (el) => {
1115
return `<i class="fa fa-star"></i>${el.innerHTML}`;
@@ -17,11 +21,37 @@ export default class Example {
1721
return divElm.textContent?.includes(search) ?? true;
1822
},
1923
}) as MultipleSelectInstance;
24+
25+
this.ms2 = multipleSelect('#from-data', {
26+
dataTest: 'select1',
27+
displayTitle: true,
28+
renderOptionLabelAsHtml: true,
29+
data: [
30+
{ value: `50"`, text: `50"` },
31+
{ value: `44'`, text: `44'` },
32+
{ value: `33`, text: `<span style="font-weight:bold">33</span>` },
33+
],
34+
}) as MultipleSelectInstance;
35+
36+
this.btnEnableElm = document.querySelector('#enableRenderHtml') as HTMLButtonElement;
37+
this.btnEnableElm.addEventListener('click', () => this.renderAsHtmlHandler(true));
38+
39+
this.btnDisableElm = document.querySelector('#disableRenderHtml') as HTMLButtonElement;
40+
this.btnDisableElm.addEventListener('click', () => this.renderAsHtmlHandler(false));
41+
}
42+
43+
renderAsHtmlHandler(enabled: boolean) {
44+
this.ms1?.refreshOptions({ renderOptionLabelAsHtml: enabled });
45+
this.ms2?.refreshOptions({ renderOptionLabelAsHtml: enabled });
2046
}
2147

2248
unmount() {
2349
// destroy ms instance(s) to avoid DOM leaks
2450
this.ms1?.destroy();
51+
this.ms2?.destroy();
2552
this.ms1 = undefined;
53+
this.ms2 = undefined;
54+
this.btnEnableElm?.removeEventListener('click', () => this.renderAsHtmlHandler(true));
55+
this.btnDisableElm?.removeEventListener('click', () => this.renderAsHtmlHandler(false));
2656
}
2757
}

lib/src/MultipleSelectInstance.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
findParent,
1010
getElementOffset,
1111
getElementSize,
12+
htmlEncode,
1213
insertAfter,
1314
toggleElement,
1415
} from './utils/domUtils';
@@ -482,6 +483,7 @@ export class MultipleSelectInstance {
482483
}
483484

484485
protected initListItem(row: any, level = 0) {
486+
const isRenderAsHtml = this.options.renderOptionLabelAsHtml || this.options.useSelectOptionLabelToHtml;
485487
const title = row?.title ? `title="${row.title}"` : '';
486488
const multiple = this.options.multiple ? 'multiple' : '';
487489
const type = this.options.single ? 'radio' : 'checkbox';
@@ -522,7 +524,7 @@ export class MultipleSelectInstance {
522524
html.push(`
523525
<li class="${`group ${classes}`.trim()}" ${style}>
524526
<label class="optgroup${this.options.single || row.disabled ? ' disabled' : ''}">
525-
${group}${row.label}
527+
${group}${isRenderAsHtml ? row.label : htmlEncode(row.label)}
526528
</label>
527529
</li>
528530
`);
@@ -557,7 +559,7 @@ export class MultipleSelectInstance {
557559
${row.selected ? ' checked="checked"' : ''}
558560
${row.disabled ? ' disabled="disabled"' : ''}
559561
>
560-
<span>${row.text}</span>
562+
<span>${isRenderAsHtml ? row.text : htmlEncode(row.text)}</span>
561563
</label>
562564
</li>
563565
`,

lib/src/utils/domUtils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,25 @@ export function insertAfter(referenceNode: HTMLElement, newNode: HTMLElement) {
169169
referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
170170
}
171171

172+
/**
173+
* HTML encode using a plain <div>
174+
* Create a in-memory div, set it's inner text(which a div can encode)
175+
* then grab the encoded contents back out. The div never exists on the page.
176+
* @param {String} inputValue - input value to be encoded
177+
* @return {String}
178+
*/
179+
export function htmlEncode(inputValue: string): string {
180+
const val = typeof inputValue === 'string' ? inputValue : String(inputValue);
181+
const entityMap: { [char: string]: string } = {
182+
'&': '&amp;',
183+
'<': '&lt;',
184+
'>': '&gt;',
185+
'"': '&quot;',
186+
"'": '&#39;',
187+
};
188+
return (val || '').toString().replace(/[&<>"']/g, (s) => entityMap[s as keyof { [char: string]: string }]);
189+
}
190+
172191
/** Display or hide matched element */
173192
export function toggleElement(elm?: HTMLElement | null, display?: boolean) {
174193
if (elm?.style) {

playwright/e2e/options27.spec.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,60 @@ import { test, expect } from '@playwright/test';
22

33
test.describe('Options 27 - Text Template', () => {
44
test('option labels & selected options shows as html', async ({ page }) => {
5+
// ms-select #1
56
await page.goto('#/options27');
6-
await page.locator('.ms-parent').click();
7-
const optionLoc1 = await page.locator('.ms-drop ul li').nth(0);
8-
optionLoc1.click();
7+
await page.locator('div[data-test=select1].ms-parent').click();
8+
const optionLoc1 = await page.locator('div[data-test=select1] .ms-drop ul li').nth(0);
9+
await optionLoc1.click();
910
await expect(optionLoc1.locator('label span')).toHaveText('January');
1011
const spanLoc1 = await optionLoc1.locator('span').innerHTML();
1112
await expect(spanLoc1).toBe('<i class="fa fa-star"></i>January');
1213

13-
const optionLoc4 = await page.locator('.ms-drop ul li').nth(3);
14-
optionLoc4.click();
15-
await expect(optionLoc4.locator('label span')).toHaveText('April');
16-
const spanLoc4 = await optionLoc4.locator('span').innerHTML();
17-
await expect(spanLoc4).toBe('<i class="fa fa-star"></i>April');
14+
const ms1OptionLoc4 = await page.locator('div[data-test=select1] .ms-drop ul li').nth(3);
15+
await ms1OptionLoc4.click();
16+
await expect(ms1OptionLoc4.locator('label span')).toHaveText('April');
17+
const ms1SpanLoc4 = await ms1OptionLoc4.locator('span').innerHTML();
18+
await expect(ms1SpanLoc4).toBe('<i class="fa fa-star"></i>April');
1819

1920
await page.waitForTimeout(90);
20-
await expect(page.locator('.ms-choice span')).toHaveText('January, April');
21-
const parentSpanLoc = await page.locator('.ms-parent .ms-choice span').innerHTML();
22-
await expect(parentSpanLoc).toBe('<i class="fa fa-star"></i>January, <i class="fa fa-star"></i>April');
21+
await expect(page.locator('div[data-test=select1] .ms-choice span')).toHaveText('January, April');
22+
let ms1ParentSpanLoc = await page.locator('div[data-test=select1].ms-parent .ms-choice span').innerHTML();
23+
await expect(ms1ParentSpanLoc).toBe('<i class="fa fa-star"></i>January, <i class="fa fa-star"></i>April');
24+
25+
await page.locator('button#disableRenderHtml').click();
26+
ms1ParentSpanLoc = (await page.locator('div[data-test=select1].ms-parent .ms-choice span').textContent()) as string;
27+
await expect(ms1ParentSpanLoc).toBe('<i class="fa fa-star"></i>January, <i class="fa fa-star"></i>April');
28+
await page.locator('div[data-test=select1].ms-parent').click();
29+
30+
// ms-select #2
31+
await page.locator('div[data-test=select2].ms-parent').click();
32+
const ms2OptionLoc2 = await page.locator('div[data-test=select2] .ms-drop ul li').nth(0);
33+
await ms2OptionLoc2.click();
34+
await expect(ms2OptionLoc2.locator('label span')).toHaveText('50"');
35+
const spanLoc2 = await ms2OptionLoc2.locator('span').innerHTML();
36+
await expect(spanLoc2).toBe('50"');
37+
38+
const ms2OptionLoc3 = await page.locator('div[data-test=select2] .ms-drop ul li').nth(2);
39+
await ms2OptionLoc3.click();
40+
await expect(ms2OptionLoc3.locator('label span').nth(0)).toHaveText('<span style="font-weight:bold">33</span>');
41+
const spanLoc4txt = await ms2OptionLoc3.locator('span').textContent();
42+
const spanLoc4html = await ms2OptionLoc3.locator('span').innerHTML();
43+
await expect(spanLoc4txt).toBe('<span style="font-weight:bold">33</span>');
44+
await expect(spanLoc4html).toBe('&lt;span style="font-weight:bold"&gt;33&lt;/span&gt;');
45+
46+
await page.waitForTimeout(90);
47+
await expect(page.locator('div[data-test=select2] .ms-choice span')).toHaveText(
48+
'50", <span style="font-weight:bold">33</span>'
49+
);
50+
let ms2ParentSpanLocText = await page.locator('div[data-test=select2].ms-parent .ms-choice span').textContent();
51+
let ms2ParentSpanLocHtml = await page.locator('div[data-test=select2].ms-parent .ms-choice span').innerHTML();
52+
await expect(ms2ParentSpanLocText).toBe('50", <span style="font-weight:bold">33</span>');
53+
await expect(ms2ParentSpanLocHtml).toBe('50", &lt;span style="font-weight:bold"&gt;33&lt;/span&gt;');
54+
55+
await page.locator('button#enableRenderHtml').click();
56+
ms2ParentSpanLocText = await page.locator('div[data-test=select2].ms-parent .ms-choice span').nth(0).textContent();
57+
ms2ParentSpanLocHtml = await page.locator('div[data-test=select2].ms-parent .ms-choice span').nth(0).innerHTML();
58+
await expect(ms2ParentSpanLocText).toBe('50", 33');
59+
await expect(ms2ParentSpanLocHtml).toBe('50", <span style="font-weight:bold">33</span>');
2360
});
2461
});

0 commit comments

Comments
 (0)