Skip to content

Commit

Permalink
feat: replace all dynamic html strings by pure HTML elements (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding authored Nov 4, 2023
1 parent 8a6359a commit adcc33d
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 97 deletions.
155 changes: 92 additions & 63 deletions lib/src/MultipleSelectInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import Constants from './constants';
import { compareObjects, deepCopy, findByParam, removeDiacritics, removeUndefined, setDataKeys, stripScripts } from './utils';
import {
applyParsedStyleToElement,
calculateAvailableSpace,
createDomElement,
findParent,
getElementOffset,
getElementSize,
htmlEncode,
insertAfter,
toggleElement,
} from './utils/domUtils';
Expand Down Expand Up @@ -228,9 +228,9 @@ export class MultipleSelectInstance {
this.choiceElm.classList.add('disabled');
}

this.selectAllName = `data-name="selectAll${name}"`;
this.selectGroupName = `data-name="selectGroup${name}"`;
this.selectItemName = `data-name="selectItem${name}"`;
this.selectAllName = `selectAll${name}`;
this.selectGroupName = `selectGroup${name}`;
this.selectItemName = `selectItem${name}`;

if (!this.options.keepOpen) {
this._bindEventService.unbind(document.body, 'click');
Expand Down Expand Up @@ -387,7 +387,12 @@ export class MultipleSelectInstance {
const saLabelElm = document.createElement('label');
createDomElement(
'input',
{ type: 'checkbox', checked: this.allSelected, dataset: { name: `selectAll${selectName}` } },
{
type: 'checkbox',
ariaChecked: String(this.allSelected),
checked: this.allSelected,
dataset: { name: `selectAll${selectName}` },
},
saLabelElm
);
saLabelElm.appendChild(createDomElement('span', { textContent: this.formatSelectAll() }));
Expand Down Expand Up @@ -460,7 +465,9 @@ export class MultipleSelectInstance {
}
} else {
if (this.ulElm) {
this.ulElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(rows.join('')) : rows.join('');
const htmlRows: string[] = [];
rows.forEach((rowElm) => htmlRows.push(rowElm.outerHTML));
this.ulElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(htmlRows.join('')) : htmlRows.join('');
}
this.updateDataStart = 0;
this.updateDataEnd = this.updateData.length;
Expand All @@ -470,21 +477,21 @@ export class MultipleSelectInstance {
}

protected getListRows() {
const rows = [];
const rows: HTMLElement[] = [];

this.updateData = [];
this.data?.forEach((row) => {
rows.push(...this.initListItem(row));
});

rows.push(`<li class="ms-no-results">${this.formatNoMatchesFound()}</li>`);
rows.push(createDomElement('li', { className: 'ms-no-results', textContent: this.formatNoMatchesFound() }));

return rows;
}

protected initListItem(row: any, level = 0) {
protected initListItem(row: any, level = 0): HTMLElement[] {
const isRenderAsHtml = this.options.renderOptionLabelAsHtml || this.options.useSelectOptionLabelToHtml;
const title = row?.title ? `title="${row.title}"` : '';
const title = row?.title || '';
const multiple = this.options.multiple ? 'multiple' : '';
const type = this.options.single ? 'radio' : 'checkbox';
let classes = '';
Expand All @@ -505,75 +512,100 @@ export class MultipleSelectInstance {

if (row.type === 'optgroup') {
const customStyle = this.options.styler(row);
const style = customStyle ? `style="${customStyle}"` : '';
const html = [];
const group =
const styleStr = String(customStyle || '');
const htmlElms: HTMLElement[] = [];
const groupElm =
this.options.hideOptgroupCheckboxes || this.options.single
? `<span ${this.selectGroupName} data-key="${row._key}"></span>`
: `<input type="checkbox"
${this.selectGroupName}
data-key="${row._key}"
${row.selected ? ' checked="checked"' : ''}
${row.disabled ? ' disabled="disabled"' : ''}
>`;
? createDomElement('span', { dataset: { name: this.selectGroupName, key: row._key } })
: createDomElement('input', {
type: 'checkbox',
dataset: { name: this.selectGroupName, key: row._key },
ariaChecked: String(row.selected),
checked: row.selected,
disabled: row.disabled,
});

if (!classes.includes('hide-radio') && (this.options.hideOptgroupCheckboxes || this.options.single)) {
classes += 'hide-radio ';
}

html.push(`
<li class="${`group ${classes}`.trim()}" ${style}>
<label class="optgroup${this.options.single || row.disabled ? ' disabled' : ''}">
${group}${isRenderAsHtml ? row.label : htmlEncode(row.label)}
</label>
</li>
`);
const labelElm = createDomElement('label', {
className: `optgroup${this.options.single || row.disabled ? ' disabled' : ''}`,
});
labelElm.appendChild(groupElm);

const spanElm = document.createElement('span');
if (isRenderAsHtml) {
spanElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(row.label) : row.label;
} else {
spanElm.textContent = row.label;
}
labelElm.appendChild(spanElm);
const liElm = createDomElement('li', { className: `group ${classes}`.trim() });
applyParsedStyleToElement(liElm, styleStr);
liElm.appendChild(labelElm);
htmlElms.push(liElm);

(row as OptGroupRowData).children.forEach((child: any) => {
html.push(...this.initListItem(child, 1));
htmlElms.push(...this.initListItem(child, 1));
});

return html;
return htmlElms;
}

const customStyle = this.options.styler(row);
const style = customStyle ? `style="${customStyle}"` : '';
const style = String(customStyle || '');
classes += row.classes || '';

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

if (row.divider) {
return '<li class="option-divider"/>';
}

return [
`
<li ${multiple || classes ? `class="${(multiple + classes).trim()}"` : ''} ${title} ${style}>
<label ${row.disabled ? 'class="disabled"' : ''}>
<input type="${type}"
value="${encodeURI(row.value)}"
data-key="${row._key}"
${this.selectItemName}
${row.selected ? ' checked="checked"' : ''}
${row.disabled ? ' disabled="disabled"' : ''}
>
<span>${isRenderAsHtml ? row.text : htmlEncode(row.text)}</span>
</label>
</li>
`,
];
return [createDomElement('li', { className: 'option-divider' })];
}

const liElm = createDomElement('li', {
title,
className: multiple || classes ? (multiple + classes).trim() : '',
});
applyParsedStyleToElement(liElm, style);

const labelElm = createDomElement('label', { className: `${row.disabled ? 'disabled' : ''}` });
const inputElm = createDomElement('input', {
type,
value: encodeURI(row.value),
dataset: { key: row._key, name: this.selectItemName },
ariaChecked: String(row.selected),
checked: Boolean(row.selected),
disabled: Boolean(row.disabled),
});
if (row.selected) {
inputElm.setAttribute('checked', 'checked');
}

const spanElm = document.createElement('span');
if (isRenderAsHtml) {
spanElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(row.text) : row.text;
} else {
spanElm.textContent = row.text;
}

labelElm.appendChild(inputElm);
labelElm.appendChild(spanElm);
liElm.appendChild(labelElm);

return [liElm];
}

protected initSelected(ignoreTrigger = false) {
let selectedTotal = 0;

for (const row of this.data || []) {
if ((row as OptGroupRowData).type === 'optgroup') {
const selectedCount = (row as OptGroupRowData).children.filter((child) => {
return child && child.selected && !child.disabled && child.visible;
}).length;
const selectedCount = (row as OptGroupRowData).children.filter(
(child) => child && child.selected && !child.disabled && child.visible
).length;

if ((row as OptGroupRowData).children.length) {
row.selected =
Expand Down Expand Up @@ -609,7 +641,6 @@ export class MultipleSelectInstance {

if (window.getComputedStyle) {
computedWidth = window.getComputedStyle(this.elm).width;

if (computedWidth === 'auto') {
computedWidth = getElementSize(this.dropElm, 'outer', 'width') + 20;
}
Expand All @@ -632,12 +663,12 @@ export class MultipleSelectInstance {
this._bindEventService.unbind(this.noResultsElm);

this.searchInputElm = this.dropElm.querySelector<HTMLInputElement>('.ms-search input');
this.selectAllElm = this.dropElm.querySelector<HTMLInputElement>(`input[${this.selectAllName}]`);
this.selectAllElm = this.dropElm.querySelector<HTMLInputElement>(`input[data-name="${this.selectAllName}"]`);
this.selectGroupElms = this.dropElm.querySelectorAll<HTMLInputElement>(
`input[${this.selectGroupName}],span[${this.selectGroupName}]`
`input[data-name="${this.selectGroupName}"],span[data-name="${this.selectGroupName}"]`
);
this.selectItemElms = this.dropElm.querySelectorAll<HTMLInputElement>(`input[${this.selectItemName}]:enabled`);
this.disableItemElms = this.dropElm.querySelectorAll<HTMLInputElement>(`input[${this.selectItemName}]:disabled`);
this.selectItemElms = this.dropElm.querySelectorAll<HTMLInputElement>(`input[data-name="${this.selectItemName}"]:enabled`);
this.disableItemElms = this.dropElm.querySelectorAll<HTMLInputElement>(`input[data-name="${this.selectItemName}"]:disabled`);
this.noResultsElm = this.dropElm.querySelector<HTMLDivElement>('.ms-no-results');

const toggleOpen = (e: MouseEvent & { target: HTMLElement }) => {
Expand Down Expand Up @@ -708,7 +739,7 @@ export class MultipleSelectInstance {
}
});
if (visibleLiElms.length) {
const [selectItemAttrName] = this.selectItemName.split('='); // [data-name="selectItem"], we want "data-name" attribute
const selectItemAttrName = 'data-name'; // [data-name="selectItem"], we want "data-name" attribute
if (visibleLiElms[0].hasAttribute(selectItemAttrName)) {
this.setSelects([visibleLiElms[0].value]);
}
Expand Down Expand Up @@ -842,10 +873,8 @@ export class MultipleSelectInstance {
if (this.options.container instanceof Node) {
container = this.options.container as HTMLElement;
} else if (typeof this.options.container === 'string') {
// prettier-ignore
container = this.options.container === 'body'
? document.body
: document.querySelector(this.options.container) as HTMLElement;
container =
this.options.container === 'body' ? document.body : (document.querySelector(this.options.container) as HTMLElement);
}
container!.appendChild(this.dropElm);
this.dropElm.style.top = `${offset?.top ?? 0}px`;
Expand Down Expand Up @@ -1092,7 +1121,7 @@ export class MultipleSelectInstance {
let selected = false;
if (type === 'text') {
const divElm = document.createElement('div');
divElm.innerHTML = row.text;
divElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(row.text) : row.text;
selected = values.includes(divElm.textContent?.trim() ?? '');
} else {
selected = values.includes(row._value || row.value);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface OptGroupRowData extends Omit<OptionRowData, 'text' | 'value'> {
}

export interface VirtualScrollOption {
rows: string[];
rows: HTMLElement[];
scrollEl: HTMLElement;
contentEl: HTMLElement;
callback: () => void;
Expand Down
10 changes: 5 additions & 5 deletions lib/src/interfaces/multipleSelectOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface MultipleSelectOption extends MultipleSelectLocale {
/** defaults to 10, when using "autoAdjustDropHeight" we might want to add a bottom (or top) padding instead of taking the entire available space */
adjustedHeightPadding: number;

/** Use optional string to override "All selected" text instead of `formatAllSelected()`, the latter should be prefered */
/** Use optional string to override "All selected" text instead of `formatAllSelected()`, the latter should be preferred */
allSelectedText?: string;

/** Auto-adjust the Drop menu height to fit with available space */
Expand All @@ -35,7 +35,7 @@ export interface MultipleSelectOption extends MultipleSelectLocale {
/** HTML container to use for the drop menu, e.g. 'body' */
container?: string | HTMLElement | null;

/** Use optional string to override selected count text "# of % selected" instead of `formatCountSelected()`, the latter should be prefered */
/** Use optional string to override selected count text "# of % selected" instead of `formatCountSelected()`, the latter should be preferred */
countSelectedText?: string;

/** provide custom data */
Expand Down Expand Up @@ -118,10 +118,10 @@ export interface MultipleSelectOption extends MultipleSelectLocale {
/** Provide a name to the multiple select element. By default this option is set to ''. */
name?: string;

/** Use optional string to override text when filtering "No matches found" instead of `formatNoMatchesFound()`, the latter should be prefered */
/** Use optional string to override text when filtering "No matches found" instead of `formatNoMatchesFound()`, the latter should be preferred */
noMatchesFoundText?: string;

/** Use optional string to override "OK" button text instead of `formatOkButton()`, the latter should be prefered */
/** Use optional string to override "OK" button text instead of `formatOkButton()`, the latter should be preferred */
okButtonText?: string;

/** Should we open the dropdown while hovering the select? */
Expand All @@ -139,7 +139,7 @@ export interface MultipleSelectOption extends MultipleSelectLocale {
/** Whether or not Multiple Select show select all checkbox. */
selectAll?: boolean;

/** Use optional string to override "Select All" checkbox text instead of `formatSelectAll()`, the latter should be prefered */
/** Use optional string to override "Select All" checkbox text instead of `formatSelectAll()`, the latter should be preferred */
selectAllText?: string;

/** Whether or not Multiple Select allows you to select only one option.By default this option is set to false. */
Expand Down
Loading

0 comments on commit adcc33d

Please sign in to comment.