Skip to content

Commit 0f32e65

Browse files
authored
fix: Virtual Scroll not working on large dataset w/HTML render enabled, fixes #203 (#204)
1 parent 72ebb75 commit 0f32e65

File tree

6 files changed

+151
-75
lines changed

6 files changed

+151
-75
lines changed

packages/demo/src/examples/example10.html

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,24 @@ <h2 class="bd-title">
1919
</h2>
2020
<div class="demo-subtitle">Virtual scroll will be used with a large set of data</div>
2121
</div>
22-
2322
</div>
2423

2524
<div>
2625
<div class="mb-3 row">
2726
<label class="col-sm-2">
28-
Basic Select
27+
Basic Array
2928
</label>
3029

3130
<div class="col-sm-10">
32-
<select multiple="multiple" data-test="select10" id="select" class="full-width">
31+
<select multiple="multiple" data-test="select10-1" id="select1" class="full-width"></select>
3332
</div>
3433
</div>
35-
</div>
34+
35+
<div class="mb-3 row">
36+
<label class="col-sm-2 col-form-label">Object Array</label>
37+
38+
<div class="col-sm-10">
39+
<select multiple="multiple" data-test="select10-2" id="select2" class="full-width"></select>
40+
</div>
41+
</div>
42+
</div>

packages/demo/src/examples/example10.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,37 @@ import { multipleSelect, MultipleSelectInstance } from 'multiple-select-vanilla'
22

33
export default class Example {
44
ms1?: MultipleSelectInstance;
5+
ms2?: MultipleSelectInstance;
56

67
mount() {
7-
const data = [];
8+
const data1 = [];
9+
const data2 = [];
810
for (let i = 0; i < 10000; i++) {
9-
data.push(i);
11+
data1.push(i);
1012
}
13+
for (let i = 0; i < 10000; i++) {
14+
data2.push({ text: `<i class="fa fa-star"></i> Task ${i}`, value: i });
15+
}
16+
17+
this.ms1 = multipleSelect('#select1', {
18+
filter: true,
19+
data: data1,
20+
showSearchClear: true,
21+
}) as MultipleSelectInstance;
1122

12-
this.ms1 = multipleSelect('#select', {
23+
this.ms2 = multipleSelect('#select2', {
1324
filter: true,
14-
data,
25+
data: data2,
1526
showSearchClear: true,
27+
useSelectOptionLabelToHtml: true,
1628
}) as MultipleSelectInstance;
1729
}
1830

1931
unmount() {
2032
// destroy ms instance(s) to avoid DOM leaks
2133
this.ms1?.destroy();
34+
this.ms2?.destroy();
2235
this.ms1 = undefined;
36+
this.ms2 = undefined;
2337
}
2438
}

packages/demo/src/main.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
<div class="collapse navbar-collapse justify-content-end me-2" id="navbarSupportedContent">
2525
<ul class="navbar-nav">
26-
<li class="nav-item"><a href="playwright-report" class="nav-link" target="_blank">Playwright 🎭</a></li>
2726
</ul>
2827
</div>
2928
</div>

packages/multiple-select-vanilla/src/MultipleSelectInstance.ts

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -503,47 +503,47 @@ export class MultipleSelectInstance {
503503
protected getListRows(): HtmlStruct[] {
504504
const rows: HtmlStruct[] = [];
505505
this.updateData = [];
506-
this.data?.forEach((row) => rows.push(...this.initListItem(row)));
506+
this.data?.forEach((dataRow) => rows.push(...this.initListItem(dataRow)));
507507
rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound(), tabIndex: 0 } });
508508

509509
return rows;
510510
}
511511

512-
protected initListItem(row: OptionRowData | OptGroupRowData, level = 0): HtmlStruct[] {
513-
const title = row?.title || '';
512+
protected initListItem(dataRow: OptionRowData | OptGroupRowData, level = 0): HtmlStruct[] {
513+
const title = dataRow?.title || '';
514514
const multiple = this.options.multiple ? 'multiple' : '';
515515
const type = this.options.single ? 'radio' : 'checkbox';
516516
let classes = '';
517517

518-
if (!row?.visible) {
518+
if (!dataRow?.visible) {
519519
return [];
520520
}
521521

522-
this.updateData.push(row);
522+
this.updateData.push(dataRow);
523523

524524
if (this.options.single && !this.options.singleRadio) {
525525
classes = 'hide-radio ';
526526
}
527527

528-
if (row.selected) {
528+
if (dataRow.selected) {
529529
classes += 'selected ';
530530
}
531531

532-
if (row.type === 'optgroup') {
532+
if (dataRow.type === 'optgroup') {
533533
// - group option row -
534534
const htmlBlocks: HtmlStruct[] = [];
535535

536536
const itemOrGroupBlock: HtmlStruct =
537537
this.options.hideOptgroupCheckboxes || this.options.single
538-
? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: row._key } } }
538+
? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: dataRow._key } } }
539539
: {
540540
tagName: 'input',
541541
props: {
542542
type: 'checkbox',
543-
dataset: { name: this.selectGroupName, key: row._key },
544-
ariaChecked: String(row.selected || false),
545-
checked: !!row.selected,
546-
disabled: row.disabled,
543+
dataset: { name: this.selectGroupName, key: dataRow._key },
544+
ariaChecked: String(dataRow.selected || false),
545+
checked: !!dataRow.selected,
546+
disabled: dataRow.disabled,
547547
tabIndex: -1,
548548
},
549549
};
@@ -553,25 +553,24 @@ export class MultipleSelectInstance {
553553
}
554554

555555
const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} };
556-
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptGroupRowData).label);
557-
556+
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptGroupRowData).label);
558557
const liBlock: HtmlStruct = {
559558
tagName: 'li',
560559
props: {
561560
className: `group ${classes}`.trim(),
562-
tabIndex: classes.includes('hide-radio') || row.disabled ? -1 : 0,
561+
tabIndex: classes.includes('hide-radio') || dataRow.disabled ? -1 : 0,
563562
},
564563
children: [
565564
{
566565
tagName: 'label',
567-
props: { className: `optgroup${this.options.single || row.disabled ? ' disabled' : ''}` },
566+
props: { className: `optgroup${this.options.single || dataRow.disabled ? ' disabled' : ''}` },
568567
children: [itemOrGroupBlock, spanLabelBlock],
569568
},
570569
],
571570
};
572571

573-
const customStyleRules = this.options.cssStyler(row);
574-
const customStylerStr = String(this.options.styler(row) || ''); // deprecated
572+
const customStyleRules = this.options.cssStyler(dataRow);
573+
const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated
575574
if (customStylerStr) {
576575
liBlock.props.style = convertStringStyleToElementStyle(customStylerStr);
577576
}
@@ -580,52 +579,51 @@ export class MultipleSelectInstance {
580579
}
581580
htmlBlocks.push(liBlock);
582581

583-
(row as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1)));
582+
(dataRow as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1)));
584583

585584
return htmlBlocks;
586585
}
587586

588587
// - regular row -
589-
classes += row.classes || '';
588+
classes += dataRow.classes || '';
590589

591590
if (level && this.options.single) {
592591
classes += `option-level-${level} `;
593592
}
594593

595-
if (row.divider) {
594+
if (dataRow.divider) {
596595
return [{ tagName: 'li', props: { className: 'option-divider' } } as HtmlStruct];
597596
}
598597

599598
const liClasses = multiple || classes ? (multiple + classes).trim() : '';
600-
const labelClasses = `${row.disabled ? 'disabled' : ''}`;
599+
const labelClasses = `${dataRow.disabled ? 'disabled' : ''}`;
601600
const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} };
602-
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptionRowData).text);
603-
601+
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptionRowData).text);
604602
const inputBlock: HtmlStruct = {
605603
tagName: 'input',
606604
props: {
607605
type,
608-
value: encodeURI(row.value as string),
609-
dataset: { key: row._key, name: this.selectItemName },
610-
ariaChecked: String(row.selected || false),
611-
checked: !!row.selected,
612-
disabled: !!row.disabled,
606+
value: encodeURI(dataRow.value as string),
607+
dataset: { key: dataRow._key, name: this.selectItemName },
608+
ariaChecked: String(dataRow.selected || false),
609+
checked: !!dataRow.selected,
610+
disabled: !!dataRow.disabled,
613611
tabIndex: -1,
614612
},
615613
};
616614

617-
if (row.selected) {
615+
if (dataRow.selected) {
618616
inputBlock.attrs = { checked: 'checked' };
619617
}
620618

621619
const liBlock: HtmlStruct = {
622620
tagName: 'li',
623-
props: { className: liClasses, title, tabIndex: row.disabled ? -1 : 0, dataset: { key: row._key } },
621+
props: { className: liClasses, title, tabIndex: dataRow.disabled ? -1 : 0, dataset: { key: dataRow._key } },
624622
children: [{ tagName: 'label', props: { className: labelClasses }, children: [inputBlock, spanLabelBlock] }],
625623
};
626624

627-
const customStyleRules = this.options.cssStyler(row);
628-
const customStylerStr = String(this.options.styler(row) || ''); // deprecated
625+
const customStyleRules = this.options.cssStyler(dataRow);
626+
const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated
629627
if (customStylerStr) {
630628
liBlock.props.style = convertStringStyleToElementStyle(customStylerStr);
631629
}

packages/multiple-select-vanilla/src/utils/domUtils.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,21 @@ export function createDomElement<T extends keyof HTMLElementTagNameMap, K extend
9292
* @param appendToElm
9393
*/
9494
export function createDomStructure(item: HtmlStruct, appendToElm?: HTMLElement, parentElm?: HTMLElement): HTMLElement {
95-
// innerHTML needs to be applied separately
96-
let innerHTMLStr = '';
97-
if (item.props?.innerHTML) {
98-
innerHTMLStr = item.props.innerHTML;
99-
delete item.props.innerHTML;
100-
}
95+
// to be CSP safe, we'll omit `innerHTML` and assign it manually afterward
96+
const itemPropsOmitHtml = item.props?.innerHTML ? omitProp(item.props, 'innerHTML') : item.props;
10197

102-
const elm = createDomElement(item.tagName, objectRemoveEmptyProps(item.props, ['className', 'title', 'style']), appendToElm);
98+
const elm = createDomElement(
99+
item.tagName,
100+
objectRemoveEmptyProps(itemPropsOmitHtml, ['className', 'title', 'style']),
101+
appendToElm
102+
);
103103
let parent: HTMLElement | null | undefined = parentElm;
104104
if (!parent) {
105105
parent = elm;
106106
}
107107

108-
if (innerHTMLStr) {
109-
elm.innerHTML = innerHTMLStr; // type should already be as TrustedHTML
108+
if (item.props.innerHTML) {
109+
elm.innerHTML = item.props.innerHTML; // at this point, string type should already be as TrustedHTML
110110
}
111111

112112
// add all custom DOM element attributes
@@ -247,6 +247,12 @@ export function insertAfter(referenceNode: HTMLElement, newNode: HTMLElement) {
247247
referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
248248
}
249249

250+
export function omitProp(obj: any, key: string) {
251+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
252+
const { [key]: omitted, ...rest } = obj;
253+
return rest;
254+
}
255+
250256
/** Display or hide matched element */
251257
export function toggleElement(elm?: HTMLElement | null, display?: boolean) {
252258
if (elm?.style) {

0 commit comments

Comments
 (0)