Skip to content

Commit

Permalink
fix: Virtual Scroll not working on large dataset w/HTML render enabled,
Browse files Browse the repository at this point in the history
fixes #203 (#204)
  • Loading branch information
ghiscoding authored Jan 31, 2024
1 parent 72ebb75 commit 0f32e65
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 75 deletions.
15 changes: 11 additions & 4 deletions packages/demo/src/examples/example10.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@ <h2 class="bd-title">
</h2>
<div class="demo-subtitle">Virtual scroll will be used with a large set of data</div>
</div>

</div>

<div>
<div class="mb-3 row">
<label class="col-sm-2">
Basic Select
Basic Array
</label>

<div class="col-sm-10">
<select multiple="multiple" data-test="select10" id="select" class="full-width">
<select multiple="multiple" data-test="select10-1" id="select1" class="full-width"></select>
</div>
</div>
</div>

<div class="mb-3 row">
<label class="col-sm-2 col-form-label">Object Array</label>

<div class="col-sm-10">
<select multiple="multiple" data-test="select10-2" id="select2" class="full-width"></select>
</div>
</div>
</div>
22 changes: 18 additions & 4 deletions packages/demo/src/examples/example10.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,37 @@ import { multipleSelect, MultipleSelectInstance } from 'multiple-select-vanilla'

export default class Example {
ms1?: MultipleSelectInstance;
ms2?: MultipleSelectInstance;

mount() {
const data = [];
const data1 = [];
const data2 = [];
for (let i = 0; i < 10000; i++) {
data.push(i);
data1.push(i);
}
for (let i = 0; i < 10000; i++) {
data2.push({ text: `<i class="fa fa-star"></i> Task ${i}`, value: i });
}

this.ms1 = multipleSelect('#select1', {
filter: true,
data: data1,
showSearchClear: true,
}) as MultipleSelectInstance;

this.ms1 = multipleSelect('#select', {
this.ms2 = multipleSelect('#select2', {
filter: true,
data,
data: data2,
showSearchClear: true,
useSelectOptionLabelToHtml: true,
}) as MultipleSelectInstance;
}

unmount() {
// destroy ms instance(s) to avoid DOM leaks
this.ms1?.destroy();
this.ms2?.destroy();
this.ms1 = undefined;
this.ms2 = undefined;
}
}
1 change: 0 additions & 1 deletion packages/demo/src/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

<div class="collapse navbar-collapse justify-content-end me-2" id="navbarSupportedContent">
<ul class="navbar-nav">
<li class="nav-item"><a href="playwright-report" class="nav-link" target="_blank">Playwright 🎭</a></li>
</ul>
</div>
</div>
Expand Down
64 changes: 31 additions & 33 deletions packages/multiple-select-vanilla/src/MultipleSelectInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,47 +503,47 @@ export class MultipleSelectInstance {
protected getListRows(): HtmlStruct[] {
const rows: HtmlStruct[] = [];
this.updateData = [];
this.data?.forEach((row) => rows.push(...this.initListItem(row)));
this.data?.forEach((dataRow) => rows.push(...this.initListItem(dataRow)));
rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound(), tabIndex: 0 } });

return rows;
}

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

if (!row?.visible) {
if (!dataRow?.visible) {
return [];
}

this.updateData.push(row);
this.updateData.push(dataRow);

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

if (row.selected) {
if (dataRow.selected) {
classes += 'selected ';
}

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

const itemOrGroupBlock: HtmlStruct =
this.options.hideOptgroupCheckboxes || this.options.single
? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: row._key } } }
? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: dataRow._key } } }
: {
tagName: 'input',
props: {
type: 'checkbox',
dataset: { name: this.selectGroupName, key: row._key },
ariaChecked: String(row.selected || false),
checked: !!row.selected,
disabled: row.disabled,
dataset: { name: this.selectGroupName, key: dataRow._key },
ariaChecked: String(dataRow.selected || false),
checked: !!dataRow.selected,
disabled: dataRow.disabled,
tabIndex: -1,
},
};
Expand All @@ -553,25 +553,24 @@ export class MultipleSelectInstance {
}

const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} };
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptGroupRowData).label);

this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptGroupRowData).label);
const liBlock: HtmlStruct = {
tagName: 'li',
props: {
className: `group ${classes}`.trim(),
tabIndex: classes.includes('hide-radio') || row.disabled ? -1 : 0,
tabIndex: classes.includes('hide-radio') || dataRow.disabled ? -1 : 0,
},
children: [
{
tagName: 'label',
props: { className: `optgroup${this.options.single || row.disabled ? ' disabled' : ''}` },
props: { className: `optgroup${this.options.single || dataRow.disabled ? ' disabled' : ''}` },
children: [itemOrGroupBlock, spanLabelBlock],
},
],
};

const customStyleRules = this.options.cssStyler(row);
const customStylerStr = String(this.options.styler(row) || ''); // deprecated
const customStyleRules = this.options.cssStyler(dataRow);
const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated
if (customStylerStr) {
liBlock.props.style = convertStringStyleToElementStyle(customStylerStr);
}
Expand All @@ -580,52 +579,51 @@ export class MultipleSelectInstance {
}
htmlBlocks.push(liBlock);

(row as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1)));
(dataRow as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1)));

return htmlBlocks;
}

// - regular row -
classes += row.classes || '';
classes += dataRow.classes || '';

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

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

const liClasses = multiple || classes ? (multiple + classes).trim() : '';
const labelClasses = `${row.disabled ? 'disabled' : ''}`;
const labelClasses = `${dataRow.disabled ? 'disabled' : ''}`;
const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} };
this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptionRowData).text);

this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptionRowData).text);
const inputBlock: HtmlStruct = {
tagName: 'input',
props: {
type,
value: encodeURI(row.value as string),
dataset: { key: row._key, name: this.selectItemName },
ariaChecked: String(row.selected || false),
checked: !!row.selected,
disabled: !!row.disabled,
value: encodeURI(dataRow.value as string),
dataset: { key: dataRow._key, name: this.selectItemName },
ariaChecked: String(dataRow.selected || false),
checked: !!dataRow.selected,
disabled: !!dataRow.disabled,
tabIndex: -1,
},
};

if (row.selected) {
if (dataRow.selected) {
inputBlock.attrs = { checked: 'checked' };
}

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

const customStyleRules = this.options.cssStyler(row);
const customStylerStr = String(this.options.styler(row) || ''); // deprecated
const customStyleRules = this.options.cssStyler(dataRow);
const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated
if (customStylerStr) {
liBlock.props.style = convertStringStyleToElementStyle(customStylerStr);
}
Expand Down
24 changes: 15 additions & 9 deletions packages/multiple-select-vanilla/src/utils/domUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,21 @@ export function createDomElement<T extends keyof HTMLElementTagNameMap, K extend
* @param appendToElm
*/
export function createDomStructure(item: HtmlStruct, appendToElm?: HTMLElement, parentElm?: HTMLElement): HTMLElement {
// innerHTML needs to be applied separately
let innerHTMLStr = '';
if (item.props?.innerHTML) {
innerHTMLStr = item.props.innerHTML;
delete item.props.innerHTML;
}
// to be CSP safe, we'll omit `innerHTML` and assign it manually afterward
const itemPropsOmitHtml = item.props?.innerHTML ? omitProp(item.props, 'innerHTML') : item.props;

const elm = createDomElement(item.tagName, objectRemoveEmptyProps(item.props, ['className', 'title', 'style']), appendToElm);
const elm = createDomElement(
item.tagName,
objectRemoveEmptyProps(itemPropsOmitHtml, ['className', 'title', 'style']),
appendToElm
);
let parent: HTMLElement | null | undefined = parentElm;
if (!parent) {
parent = elm;
}

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

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

export function omitProp(obj: any, key: string) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: omitted, ...rest } = obj;
return rest;
}

/** Display or hide matched element */
export function toggleElement(elm?: HTMLElement | null, display?: boolean) {
if (elm?.style) {
Expand Down
Loading

0 comments on commit 0f32e65

Please sign in to comment.