Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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