multiple-select-vanilla
Version:
This lib allows you to select multiple elements with checkboxes
4 lines • 153 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../src/index.ts", "../../src/services/binding-event.service.ts", "../../src/locales/multiple-select-en-US.ts", "../../src/constants.ts", "../../src/utils/utils.ts", "../../src/utils/domUtils.ts", "../../src/services/virtual-scroll.ts", "../../src/MultipleSelectInstance.ts", "../../src/multiple-select.ts"],
"sourcesContent": ["export type * from './models/interfaces.js';\nexport type * from './models/locale.interface.js';\nexport type * from './models/multipleSelectOption.interface.js';\nexport { BindingEventService, type ElementEventListener } from './services/binding-event.service.js';\nexport { VirtualScroll } from './services/virtual-scroll.js';\nexport {\n type HtmlElementPosition,\n calculateAvailableSpace,\n classNameToList,\n convertItemRowToHtml,\n createDomElement,\n createDomStructure,\n emptyElement,\n findParent,\n getElementOffset,\n getElementSize,\n insertAfter,\n omitProp,\n toggleElement,\n toggleElementClass,\n windowScrollPosition,\n} from './utils/domUtils.js';\nexport {\n compareObjects,\n deepCopy,\n findByParam,\n isDefined,\n objectRemoveEmptyProps,\n removeDiacritics,\n removeUndefined,\n setDataKeys,\n stripScripts,\n toCamelCase,\n} from './utils/utils.js';\nexport { multipleSelect } from './multiple-select.js';\nexport { MultipleSelectInstance } from './MultipleSelectInstance.js';\n", "export interface ElementEventListener {\n element: Element;\n eventName: keyof HTMLElementEventMap;\n listener: EventListener;\n groupName?: string;\n}\n\nexport class BindingEventService {\n protected _distinctEvent: boolean;\n protected _boundedEvents: ElementEventListener[] = [];\n\n get boundedEvents(): ElementEventListener[] {\n return this._boundedEvents;\n }\n\n constructor(options?: { distinctEvent: boolean }) {\n this._distinctEvent = options?.distinctEvent ?? false;\n }\n\n dispose() {\n this.unbindAll();\n this._boundedEvents = [];\n }\n\n /** Bind an event listener to any element */\n bind<H extends HTMLElement = HTMLElement>(\n elementOrElements: H | NodeListOf<H> | Window,\n eventNameOrNames: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>,\n listener: EventListener,\n listenerOptions?: boolean | AddEventListenerOptions,\n groupName = '',\n ) {\n // convert to array for looping in next task\n const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames];\n\n if (typeof (elementOrElements as NodeListOf<H>)?.forEach === 'function') {\n // multiple elements to bind to\n (elementOrElements as NodeListOf<H>).forEach(element => {\n for (const eventName of eventNames) {\n if (!this._distinctEvent || (this._distinctEvent && !this.hasBinding(element, eventName))) {\n element.addEventListener(eventName, listener as EventListener, listenerOptions);\n this._boundedEvents.push({ element, eventName, listener: listener as EventListener, groupName });\n }\n }\n });\n } else {\n // single elements to bind to\n for (const eventName of eventNames) {\n if (!this._distinctEvent || (this._distinctEvent && !this.hasBinding(elementOrElements as H, eventName))) {\n (elementOrElements as H).addEventListener(eventName, listener as EventListener, listenerOptions);\n this._boundedEvents.push({\n element: elementOrElements as H,\n eventName,\n listener: listener as EventListener,\n groupName,\n });\n }\n }\n }\n }\n\n hasBinding(elm: Element, eventNameOrNames?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>): boolean {\n return this._boundedEvents.some(f => f.element === elm && (!eventNameOrNames || f.eventName === eventNameOrNames));\n }\n\n /** Unbind a specific listener that was bounded earlier */\n unbind(\n elementOrElements?: Element | NodeListOf<Element> | null,\n eventNameOrNames?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>,\n listener?: EventListenerOrEventListenerObject | null,\n ) {\n if (elementOrElements) {\n const elements = Array.isArray(elementOrElements) ? elementOrElements : [elementOrElements];\n const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames || '' : [eventNameOrNames || ''];\n\n for (const element of elements) {\n if (!listener) {\n listener = this._boundedEvents.find(f => {\n if (f.element === element && (!eventNameOrNames || f.eventName === eventNameOrNames)) {\n return f.listener;\n }\n return undefined;\n }) as EventListener | undefined;\n }\n\n for (const eventName of eventNames) {\n element?.removeEventListener?.(eventName, listener);\n }\n }\n }\n }\n\n /**\n * Unbind all event listeners that were bounded, optionally provide a group name to unbind all listeners assigned to that specific group only.\n */\n unbindAll(groupName?: string | string[]) {\n if (groupName) {\n const groupNames = Array.isArray(groupName) ? groupName : [groupName];\n // unbind only the bounded event with a specific group\n // Note: we need to loop in reverse order to avoid array reindexing (causing index offset) after a splice is called\n for (let i = this._boundedEvents.length - 1; i >= 0; --i) {\n const boundedEvent = this._boundedEvents[i];\n if (groupNames.some(g => g === boundedEvent.groupName)) {\n const { element, eventName, listener } = boundedEvent;\n this.unbind(element, eventName, listener);\n this._boundedEvents.splice(i, 1);\n }\n }\n } else {\n // unbind everything\n while (this._boundedEvents.length > 0) {\n const boundedEvent = this._boundedEvents.pop() as ElementEventListener;\n const { element, eventName, listener } = boundedEvent;\n this.unbind(element, eventName, listener);\n }\n }\n }\n}\n", "/**\n * Multiple Select en-US translation\n * Author: Zhixin Wen<wenzhixin2010@gmail.com>\n */\n\nimport type { MultipleSelectInstance } from '../MultipleSelectInstance.js';\nimport type { MultipleSelectLocale, MultipleSelectLocales } from '../models/locale.interface.js';\n\nconst ms =\n typeof window !== 'undefined' && window.multipleSelect !== undefined\n ? window.multipleSelect\n : ({ locales: {} as MultipleSelectLocales } as Partial<MultipleSelectInstance>);\n\nexport const English = {\n formatSelectAll() {\n return '[Select all]';\n },\n formatAllSelected() {\n return 'All selected';\n },\n formatCountSelected(count: number, total: number) {\n return `${count} of ${total} selected`;\n },\n formatNoMatchesFound() {\n return 'No matches found';\n },\n formatOkButton() {\n return 'OK';\n },\n} as MultipleSelectLocale;\n\n(ms.locales as MultipleSelectLocales)['en-US'] = English;\n\nexport default ms.locales;\n", "import type { LabelFilter, TextFilter } from './models/interfaces.js';\nimport type { MultipleSelectOption } from './models/multipleSelectOption.interface.js';\nimport English from './locales/multiple-select-en-US.js';\n\nconst BLOCK_ROWS = 50;\nconst CLUSTER_BLOCKS = 4;\n\nconst DEFAULTS: Partial<MultipleSelectOption> = {\n name: '',\n placeholder: '',\n classes: '',\n classPrefix: '',\n data: undefined,\n locale: undefined,\n\n selectAll: true,\n single: undefined,\n singleRadio: false,\n multiple: false,\n hideOptgroupCheckboxes: false,\n multipleWidth: 80,\n width: undefined,\n dropWidth: undefined,\n maxHeight: 250,\n maxHeightUnit: 'px',\n position: 'bottom',\n\n displayValues: false,\n displayTitle: false,\n displayDelimiter: ', ',\n minimumCountSelected: 3,\n ellipsis: false,\n\n isOpen: false,\n keepOpen: false,\n openOnHover: false,\n container: null,\n\n filter: false,\n filterGroup: false,\n filterPlaceholder: '',\n filterAcceptOnEnter: false,\n filterByDataLength: undefined,\n customFilter(filterOptions) {\n const { text, label, search } = filterOptions as LabelFilter & TextFilter;\n return (label || text || '').includes(search);\n },\n\n showClear: false,\n\n // auto-position the drop\n autoAdjustDropHeight: false,\n autoAdjustDropPosition: false,\n autoAdjustDropWidthByTextSize: false,\n adjustedHeightPadding: 10,\n useSelectOptionLabel: false,\n useSelectOptionLabelToHtml: false,\n\n navigationHighlight: true,\n infiniteScroll: false,\n virtualScroll: true,\n\n cssStyler: () => null,\n textTemplate: (elm: HTMLOptionElement) => elm.innerHTML.trim(),\n labelTemplate: (elm: HTMLOptionElement) => elm.label,\n\n onOpen: () => false,\n onClose: () => false,\n onCheckAll: () => false,\n onUncheckAll: () => false,\n onFocus: () => false,\n onBlur: () => false,\n onOptgroupClick: () => false,\n onBeforeClick: () => true,\n onClick: () => false,\n onFilter: () => false,\n onFilterClear: () => false,\n onClear: () => false,\n onAfterCreate: () => false,\n onDestroy: () => false,\n onAfterDestroy: () => false,\n onDestroyed: () => false,\n};\n\nconst METHODS = [\n 'init',\n 'getOptions',\n 'refreshOptions',\n 'getSelects',\n 'setSelects',\n 'enable',\n 'disable',\n 'open',\n 'close',\n 'check',\n 'uncheck',\n 'checkAll',\n 'uncheckAll',\n 'checkInvert',\n 'focus',\n 'blur',\n 'refresh',\n 'destroy',\n];\n\nObject.assign(DEFAULTS, English!['en-US']); // load English as default locale\n\nconst Constants = {\n BLOCK_ROWS,\n CLUSTER_BLOCKS,\n DEFAULTS,\n METHODS,\n};\n\nexport default Constants;\n", "/** Compare two objects */\nexport function compareObjects(objectA: any, objectB: any, compareLength = false) {\n const aKeys = Object.keys(objectA);\n const bKeys = Object.keys(objectB);\n\n if (compareLength && aKeys.length !== bKeys.length) {\n return false;\n }\n\n for (const key of aKeys) {\n if (bKeys.includes(key) && objectA[key] !== objectB[key]) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Create an immutable clone of an array or object\n * (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com\n * @param {Array|Object} objectOrArray - the array or object to copy\n * @return {Array|Object} - the clone of the array or object\n */\nexport function deepCopy(objectOrArray: any | any[]): any | any[] {\n const cloneObj = () => {\n const clone = {}; // create new object\n\n // Loop through each item in the original, recursively copy it's value and add to the clone\n // eslint-disable-next-line no-restricted-syntax\n for (const key in objectOrArray) {\n if (Object.prototype.hasOwnProperty.call(objectOrArray, key)) {\n (clone as any)[key] = deepCopy(objectOrArray[key]);\n }\n }\n return clone;\n };\n\n // Create an immutable copy of an array\n const cloneArr = () => objectOrArray.map((item: any) => deepCopy(item));\n\n // Get object type\n const type = Object.prototype.toString.call(objectOrArray).slice(8, -1).toLowerCase();\n if (type === 'object') {\n return cloneObj(); // if it's an object\n }\n if (type === 'array') {\n return cloneArr(); // if it's an array\n }\n return objectOrArray; // otherwise, return it as-is, could be primitive or else\n}\n\nexport function isDefined(val: any) {\n return val !== undefined && val !== null && val !== '';\n}\n\n/**\n * Remove all empty props from an object,\n * we can optionally provide a fixed list of props to consider for removal (anything else will be excluded)\n * @param {*} obj\n * @param {Array<String>} [clearProps] - optional list of props to consider for removal (anything else will be excluded)\n * @returns cleaned object\n */\nexport function objectRemoveEmptyProps(obj: any, clearProps?: string[]) {\n if (typeof obj === 'object') {\n if (clearProps) {\n return Object.fromEntries(Object.entries(obj).filter(([name, val]) => (!isDefined(val) && !clearProps.includes(name)) || isDefined(val)));\n }\n return Object.fromEntries(Object.entries(obj).filter(([_, v]) => isDefined(v)));\n }\n return obj;\n}\n\nexport function setDataKeys(data: any[]) {\n let total = 0;\n\n data.forEach((row, i) => {\n if (row.type === 'optgroup') {\n row._key = `group_${i}`;\n row.visible = typeof row.visible === 'undefined' ? true : row.visible;\n\n row.children.forEach((child: any, j: number) => {\n if (child) {\n child.visible = typeof child?.visible === 'undefined' ? true : child.visible;\n\n if (!child.divider) {\n child._key = `option_${i}_${j}`;\n total += 1;\n }\n }\n });\n } else {\n row.visible = typeof row.visible === 'undefined' ? true : row.visible;\n\n if (!row.divider) {\n row._key = `option_${i}`;\n total += 1;\n }\n }\n });\n\n return total;\n}\n\nexport function findByParam(data: any, param: any, value: any) {\n if (Array.isArray(data)) {\n for (const row of data) {\n if (row[param] === value || (row[param] === `${+row[param]}` && +row[param] === value)) {\n return row;\n }\n if (row.type === 'optgroup') {\n for (const child of row.children) {\n if (child && (child[param] === value || (child[param] === `${+child[param]}` && +child[param] === value))) {\n return child;\n }\n }\n }\n }\n }\n}\n\nexport function stripScripts(dirtyHtml: string) {\n return dirtyHtml.replace(\n /(\\b)(on[a-z]+)(\\s*)=([^>]*)|javascript:([^>]*)[^>]*|(<\\s*)(\\/*)script([<>]*).*(<\\s*)(\\/*)script(>*)|(<|<)(\\/*)(script|script defer)(.*)(>|>|>\">)/gi,\n '',\n );\n}\n\nexport function removeUndefined(obj: any) {\n Object.keys(obj).forEach(key => (obj[key] === undefined ? delete obj[key] : ''));\n return obj;\n}\n\nexport function toCamelCase(str: string) {\n return str.replace(/[\\W_]+(.)/g, (_match, char) => char.toUpperCase());\n}\n\nexport function removeDiacritics(str: string, customParser?: (t: string) => string): string {\n if (typeof str !== 'string') {\n return str;\n }\n if (typeof customParser === 'function') {\n return customParser(str);\n }\n if (typeof str.normalize === 'function') {\n return str.normalize('NFD').replace(/[\\u0300-\\u036F]/g, '');\n }\n throw new Error(\n '[Multiple-Select-Vanilla] `normalize()` function is not defined, you can optionally provide a custom parser via the `diacriticParser` option.',\n );\n}\n", "import type { HtmlStruct, InferDOMType } from '../models/interfaces.js';\nimport { objectRemoveEmptyProps } from './utils.js';\n\nexport interface HtmlElementPosition {\n top: number;\n bottom: number;\n left: number;\n right: number;\n}\n\n/** calculate available space for each side of the DOM element */\nexport function calculateAvailableSpace(element: HTMLElement): { top: number; bottom: number; left: number; right: number } {\n let bottom = 0;\n let top = 0;\n let left = 0;\n let right = 0;\n\n const windowHeight = window.innerHeight ?? 0;\n const windowWidth = window.innerWidth ?? 0;\n const scrollPosition = windowScrollPosition();\n const pageScrollTop = scrollPosition.top;\n const pageScrollLeft = scrollPosition.left;\n const elmOffset = getElementOffset(element);\n\n if (elmOffset) {\n const elementOffsetTop = elmOffset.top ?? 0;\n const elementOffsetLeft = elmOffset.left ?? 0;\n top = elementOffsetTop - pageScrollTop;\n bottom = windowHeight - (elementOffsetTop - pageScrollTop);\n left = elementOffsetLeft - pageScrollLeft;\n right = windowWidth - (elementOffsetLeft - pageScrollLeft);\n }\n\n return { top, bottom, left, right };\n}\n\n/**\n * Accepts string containing the class or space-separated list of classes, and\n * returns list of individual classes.\n * Method properly takes into account extra whitespaces in the `className`\n * e.g.: \" class1 class2 \" => will result in `['class1', 'class2']`.\n * @param {String} className - space separated list of class names\n */\nexport function classNameToList(className = ''): string[] {\n return className.split(' ').filter(cls => cls); // filter will remove whitespace entries\n}\n\n/**\n * Create a DOM Element with any optional attributes or properties.\n * It will only accept valid DOM element properties that `createElement` would accept.\n * For example: `createDomElement('div', { className: 'my-css-class' })`,\n * for style or dataset you need to use nested object `{ style: { display: 'none' }}\n * The last argument is to optionally append the created element to a parent container element.\n * @param {String} tagName - html tag\n * @param {Object} options - element properties\n * @param {[HTMLElement]} appendToParent - parent element to append to\n */\nexport function createDomElement<T extends keyof HTMLElementTagNameMap, K extends keyof HTMLElementTagNameMap[T]>(\n tagName: T,\n elementOptions?: { [P in K]: InferDOMType<HTMLElementTagNameMap[T][P]> },\n appendToParent?: HTMLElement,\n): HTMLElementTagNameMap[T] {\n const elm = document.createElement<T>(tagName);\n\n if (elementOptions) {\n Object.keys(elementOptions).forEach(elmOptionKey => {\n const elmValue = elementOptions[elmOptionKey as keyof typeof elementOptions];\n if (typeof elmValue === 'object') {\n Object.assign(elm[elmOptionKey as K] as object, elmValue);\n } else {\n elm[elmOptionKey as K] = (elementOptions as any)[elmOptionKey as keyof typeof elementOptions];\n }\n });\n }\n if (appendToParent?.appendChild) {\n appendToParent.appendChild(elm);\n }\n return elm;\n}\n\n/**\n * From an HtmlBlock, create the DOM structure and append it to dedicated DOM element, for example:\n * `{ tagName: 'li', props: { className: 'some-class' }, attrs: { 'aria-label': 'some label' }, children: [] }`\n * @param item\n * @param appendToElm\n */\nexport function createDomStructure(item: HtmlStruct, appendToElm?: HTMLElement, parentElm?: HTMLElement): HTMLElement {\n // to be CSP safe, we'll omit `innerHTML` and assign it manually afterward\n const itemPropsOmitHtml = item.props?.innerHTML ? omitProp(item.props, 'innerHTML') : item.props;\n\n const elm = createDomElement(item.tagName, objectRemoveEmptyProps(itemPropsOmitHtml, ['className', 'title', 'style']), appendToElm);\n let parent: HTMLElement | null | undefined = parentElm;\n if (!parent) {\n parent = elm;\n }\n\n if (item.props.innerHTML) {\n elm.innerHTML = item.props.innerHTML; // at this point, string type should already be as TrustedHTML\n }\n\n // add all custom DOM element attributes\n if (item.attrs) {\n for (const attrName of Object.keys(item.attrs)) {\n elm.setAttribute(attrName, item.attrs[attrName]);\n }\n }\n\n // use recursion when finding item children\n if (item.children) {\n for (const childItem of item.children) {\n createDomStructure(childItem, elm, parent);\n }\n }\n\n appendToElm?.appendChild(elm);\n return elm;\n}\n\n/** takes an html block object and converts to a real HTMLElement */\nexport function convertItemRowToHtml(item: HtmlStruct): HTMLElement {\n if (item.hasOwnProperty('tagName')) {\n return createDomStructure(item);\n }\n return document.createElement('li');\n}\n\n/**\n * Empty a DOM element by removing all of its DOM element children leaving with an empty element (basically an empty shell)\n * @return {object} element - updated element\n */\nexport function emptyElement<T extends Element = Element>(element?: T | null): T | undefined | null {\n while (element?.firstChild) {\n if (element.lastChild) {\n element.removeChild(element.lastChild);\n }\n }\n return element;\n}\n\n/** Get HTML element offset with pure JS */\nexport function getElementOffset(element?: HTMLElement): HtmlElementPosition | undefined {\n if (!element) {\n return undefined;\n }\n const rect = element?.getBoundingClientRect?.();\n let top = 0;\n let left = 0;\n let bottom = 0;\n let right = 0;\n\n if (rect?.top !== undefined && rect.left !== undefined) {\n top = rect.top + window.pageYOffset;\n left = rect.left + window.pageXOffset;\n right = rect.right;\n bottom = rect.bottom;\n }\n return { top, left, bottom, right };\n}\n\nexport function getElementSize(elm: HTMLElement | undefined, mode: 'inner' | 'outer' | 'scroll', type: 'height' | 'width') {\n if (!elm) {\n return 0;\n }\n\n // first try defined style width or offsetWidth (which include scroll & padding)\n let size = Number.parseFloat(elm.style[type]);\n if (!size || Number.isNaN(size)) {\n switch (mode) {\n case 'outer':\n size = elm[type === 'width' ? 'offsetWidth' : 'offsetHeight'];\n break;\n case 'scroll':\n size = elm[type === 'width' ? 'scrollWidth' : 'scrollHeight'];\n break;\n case 'inner':\n default:\n size = elm[type === 'width' ? 'clientWidth' : 'clientHeight'];\n break;\n }\n size = elm.getBoundingClientRect()[type];\n }\n\n if (!size || Number.isNaN(size)) {\n // when 0 width, we'll try different ways\n // when element is auto or 0, we'll keep previous style values to get width and then reapply original values\n const prevDisplay = elm.style.display;\n const prevPosition = elm.style.position;\n elm.style.display = 'block';\n elm.style.position = 'absolute';\n const widthStr = window.getComputedStyle(elm)[type];\n size = Number.parseFloat(widthStr);\n if (Number.isNaN(size)) {\n size = 0;\n }\n\n // reapply original values\n elm.style.display = prevDisplay;\n elm.style.position = prevPosition;\n }\n\n return size || 0;\n}\n\n/**\n * Find a single parent by a simple selector, it only works with a simple selector\n * for example: \"input.some-class\", \".some-class\", \"input#some-id\"\n * Note: it won't work with complex selector like \"div.some-class input.my-class\"\n * @param elm\n * @param selector\n * @returns\n */\nexport function findParent(elm: HTMLElement, selector: string) {\n let targetElm: HTMLElement | null = null;\n let parentElm = elm?.parentElement;\n\n while (parentElm) {\n // query selector id (#some-id) or class (.some-class other-class)\n const [_, nodeType, selectorType, classOrIdName] = selector.match(/^([a-z]*)([#.]{1})([a-z\\-]+)$/i) || [];\n if (selectorType && classOrIdName) {\n // class or id selector type\n for (const q of classOrIdName.replace(selectorType, '').split(' ')) {\n if (parentElm.classList.contains(q)) {\n if (nodeType) {\n if (parentElm?.tagName.toLowerCase() === nodeType) {\n targetElm = parentElm;\n }\n } else {\n targetElm = parentElm;\n }\n }\n }\n }\n parentElm = parentElm.parentElement;\n }\n\n return targetElm;\n}\n\nexport function insertAfter(referenceNode: HTMLElement, newNode: HTMLElement) {\n referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);\n}\n\nexport function omitProp(obj: any, key: string) {\n const { [key]: omitted, ...rest } = obj;\n return rest;\n}\n\n/** Display or hide matched element */\nexport function toggleElement(elm?: HTMLElement | null, display?: boolean) {\n if (elm?.style) {\n elm.style.display = (elm.style.display === 'none' && display !== false) || display === true ? 'block' : 'none';\n }\n}\n\nexport function toggleElementClass(elm?: HTMLElement | null, state?: boolean) {\n if (elm?.classList) {\n const adding = state === true || !elm.classList.contains('selected');\n const action = adding ? 'add' : 'remove';\n elm.classList[action]('selected');\n }\n}\n\n/**\n * Get the Window Scroll top/left Position\n * @returns\n */\nexport function windowScrollPosition(): { left: number; top: number } {\n return {\n left: window.pageXOffset || document.documentElement.scrollLeft || 0,\n top: window.pageYOffset || document.documentElement.scrollTop || 0,\n };\n}\n", "import Constants from '../constants.js';\nimport type { HtmlStruct, VirtualCache, VirtualScrollOption } from '../models/interfaces.js';\nimport { convertItemRowToHtml, emptyElement } from '../utils/domUtils.js';\n\nexport class VirtualScroll {\n protected clusterRows?: number;\n protected cache: VirtualCache;\n protected scrollEl: HTMLElement;\n protected blockHeight?: number;\n protected clusterHeight?: number;\n protected contentEl: HTMLElement;\n protected parentEl: HTMLElement | null;\n protected itemHeight?: number;\n protected lastCluster: number;\n protected scrollTop: number;\n dataStart!: number;\n dataEnd!: number;\n rows: HtmlStruct[];\n destroy: () => void;\n callback: () => void;\n sanitizer?: (dirtyHtml: string) => string;\n\n constructor(options: VirtualScrollOption) {\n this.rows = options.rows;\n this.scrollEl = options.scrollEl;\n this.contentEl = options.contentEl;\n this.parentEl = options.contentEl?.parentElement;\n this.callback = options.callback;\n\n this.cache = {} as VirtualCache;\n this.scrollTop = this.scrollEl.scrollTop;\n\n this.initDOM(this.rows);\n\n this.scrollEl.scrollTop = this.scrollTop;\n this.lastCluster = 0;\n\n const onScroll = () => {\n if (this.lastCluster !== (this.lastCluster = this.getNum())) {\n this.initDOM(this.rows);\n this.callback();\n }\n };\n\n this.scrollEl.addEventListener('scroll', onScroll, false);\n this.destroy = () => {\n this.scrollEl.removeEventListener('scroll', onScroll, false);\n emptyElement(this.contentEl);\n };\n }\n\n reset(rows: HtmlStruct[]) {\n this.lastCluster = 0;\n this.cache = {} as any;\n emptyElement(this.contentEl);\n this.initDOM(rows);\n }\n\n protected initDOM(rows: HtmlStruct[]) {\n if (typeof this.clusterHeight === 'undefined') {\n this.cache.scrollTop = this.scrollEl.scrollTop;\n const firstRowElm = convertItemRowToHtml(rows[0]);\n\n this.contentEl.appendChild(firstRowElm);\n this.contentEl.appendChild(firstRowElm);\n this.contentEl.appendChild(firstRowElm);\n this.cache.data = [rows[0]];\n this.getRowsHeight();\n }\n\n const data = this.initData(rows, this.getNum());\n const dataChanged = this.checkChanges('data', data.rows);\n const topOffsetChanged = this.checkChanges('top', data.topOffset);\n const bottomOffsetChanged = this.checkChanges('bottom', data.bottomOffset);\n\n emptyElement(this.contentEl);\n\n if (dataChanged && topOffsetChanged) {\n if (data.topOffset) {\n this.contentEl.appendChild(this.getExtra('top', data.topOffset));\n }\n data.rows.forEach(h => this.contentEl.appendChild(convertItemRowToHtml(h)));\n\n if (data.bottomOffset) {\n this.contentEl.appendChild(this.getExtra('bottom', data.bottomOffset));\n }\n } else if (bottomOffsetChanged && this.contentEl.lastChild) {\n (this.contentEl.lastChild as HTMLElement).style.height = `${data.bottomOffset}px`;\n }\n }\n\n protected getRowsHeight() {\n if (typeof this.itemHeight === 'undefined') {\n // make sure parent is not hidden before reading item list height\n const prevParentDisplay = this.parentEl?.style.display || '';\n if (this.parentEl && (prevParentDisplay === '' || prevParentDisplay === 'none')) {\n this.parentEl.style.display = 'block';\n }\n const nodes = this.contentEl.children;\n const node = nodes[Math.floor(nodes.length / 2)];\n this.itemHeight = (node as HTMLElement).offsetHeight;\n if (this.parentEl) {\n this.parentEl.style.display = prevParentDisplay;\n }\n }\n this.blockHeight = this.itemHeight * Constants.BLOCK_ROWS;\n this.clusterRows = Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS;\n this.clusterHeight = this.blockHeight * Constants.CLUSTER_BLOCKS;\n }\n\n protected getNum() {\n this.scrollTop = this.scrollEl.scrollTop;\n const blockSize = (this.clusterHeight || 0) - (this.blockHeight || 0);\n if (blockSize) {\n return Math.floor(this.scrollTop / blockSize) || 0;\n }\n return 0;\n }\n\n protected initData(rows: HtmlStruct[], num: number) {\n if (rows.length < Constants.BLOCK_ROWS) {\n return {\n topOffset: 0,\n bottomOffset: 0,\n rowsAbove: 0,\n rows,\n };\n }\n const start = Math.max((this.clusterRows! - Constants.BLOCK_ROWS) * num, 0);\n const end = start + this.clusterRows!;\n const topOffset = Math.max(start * this.itemHeight!, 0);\n const bottomOffset = Math.max((rows.length - end) * this.itemHeight!, 0);\n const thisRows: HtmlStruct[] = [];\n let rowsAbove = start;\n if (topOffset < 1) {\n rowsAbove++;\n }\n for (let i = start; i < end; i++) {\n rows[i] && thisRows.push(rows[i]);\n }\n\n this.dataStart = start;\n this.dataEnd = end;\n\n return {\n topOffset,\n bottomOffset,\n rowsAbove,\n rows: thisRows,\n };\n }\n\n protected checkChanges<T extends keyof VirtualCache>(type: T, value: VirtualCache[T]) {\n const changed = value !== this.cache[type];\n this.cache[type] = value;\n return changed;\n }\n\n protected getExtra(className: string, height: number) {\n const tag = document.createElement('li');\n tag.className = `virtual-scroll-${className}`;\n if (height) {\n tag.style.height = `${height}px`;\n }\n return tag;\n }\n}\n", "/**\n * @author zhixin wen <wenzhixin2010@gmail.com>\n */\nimport Constants from './constants.js';\nimport type { HtmlStruct, OptGroupRowData, OptionDataObject, OptionRowData } from './models/interfaces.js';\nimport type { MultipleSelectLocales } from './models/locale.interface.js';\nimport type { CloseReason, MultipleSelectOption } from './models/multipleSelectOption.interface.js';\nimport { BindingEventService } from './services/binding-event.service.js';\nimport { VirtualScroll } from './services/virtual-scroll.js';\nimport { compareObjects, deepCopy, findByParam, removeDiacritics, removeUndefined, setDataKeys, stripScripts } from './utils/utils.js';\nimport {\n calculateAvailableSpace,\n classNameToList,\n convertItemRowToHtml,\n createDomElement,\n emptyElement,\n findParent,\n getElementOffset,\n getElementSize,\n insertAfter,\n toggleElement,\n} from './utils/domUtils.js';\nimport type { HtmlElementPosition } from './utils/domUtils.js';\n\nconst OPTIONS_LIST_SELECTOR = '.ms-select-all, ul li[data-key]';\nconst OPTIONS_HIGHLIGHT_LIST_SELECTOR = '.ms-select-all.highlighted, ul li[data-key].highlighted';\n\nexport class MultipleSelectInstance {\n protected _bindEventService: BindingEventService;\n protected isAllSelected = false;\n protected isPartiallyAllSelected = false;\n protected fromHtml = false;\n protected choiceElm!: HTMLButtonElement;\n protected selectClearElm?: HTMLDivElement | null;\n protected closeElm?: HTMLElement | null;\n protected clearSearchIconElm?: HTMLElement | null;\n protected filterText = '';\n protected updateData: any[] = [];\n protected data?: Array<OptionRowData | OptGroupRowData> = [];\n protected dataTotal?: any;\n protected dropElm?: HTMLDivElement;\n protected okButtonElm?: HTMLButtonElement;\n protected filterParentElm?: HTMLDivElement | null;\n protected lastFocusedItemKey = '';\n protected lastMouseOverPosition = '';\n protected ulElm?: HTMLUListElement | null;\n protected parentElm!: HTMLDivElement;\n protected labelElm?: HTMLLabelElement | null;\n protected selectAllParentElm?: HTMLDivElement | null;\n protected selectAllElm?: HTMLInputElement | null;\n protected searchInputElm?: HTMLInputElement | null;\n protected selectGroupElms?: NodeListOf<HTMLInputElement>;\n protected selectItemElms?: NodeListOf<HTMLInputElement>;\n protected noResultsElm?: HTMLDivElement | null;\n protected options: MultipleSelectOption;\n protected selectAllName = '';\n protected selectGroupName = '';\n protected selectItemName = '';\n protected scrolledByMouse = false;\n protected openDelayTimer?: number;\n\n protected updateDataStart?: number;\n protected updateDataEnd?: number;\n protected virtualScroll?: VirtualScroll | null;\n protected _currentHighlightIndex = -1;\n protected _currentSelectedElm?: HTMLLIElement | HTMLDivElement;\n protected isMoveUpRecalcRequired = false;\n locales = {} as MultipleSelectLocales;\n\n get isRenderAsHtml() {\n return this.options.renderOptionLabelAsHtml || this.options.useSelectOptionLabelToHtml;\n }\n\n constructor(\n protected elm: HTMLSelectElement,\n options?: Partial<Omit<MultipleSelectOption, 'onHardDestroy' | 'onAfterHardDestroy'>>,\n ) {\n this.options = Object.assign({}, Constants.DEFAULTS, this.elm.dataset, options) as MultipleSelectOption;\n this._bindEventService = new BindingEventService({ distinctEvent: true });\n }\n\n init() {\n this.initLocale();\n this.initContainer();\n this.initData();\n this.initSelected(true);\n this.initFilter();\n this.initDrop();\n this.initView();\n this.options.onAfterCreate();\n }\n\n /**\n * destroy the element, if a hard destroy is enabled then we'll also nullify it on the multipleSelect instance array.\n * When a soft destroy is called, we'll only remove it from the DOM but we'll keep all multipleSelect instances\n */\n destroy(hardDestroy = true) {\n if (this.elm && this.parentElm) {\n this.options.onDestroy({ hardDestroy });\n if (hardDestroy) {\n this.options.onHardDestroy();\n }\n if (this.elm.parentElement && this.parentElm.parentElement) {\n this.elm.parentElement.insertBefore(this.elm, this.parentElm.parentElement!.firstChild);\n }\n this.elm.classList.remove('ms-offscreen');\n this._bindEventService.unbindAll();\n\n this.virtualScroll?.destroy();\n this.dropElm?.remove();\n this.dropElm = undefined;\n this.parentElm.parentNode?.removeChild(this.parentElm);\n\n if (this.fromHtml) {\n delete this.options.data;\n this.fromHtml = false;\n }\n this.options.onAfterDestroy({ hardDestroy });\n\n // on a hard destroy, we will also nullify all variables & call event so that _multipleSelect can also nullify its own instance\n if (hardDestroy) {\n this.options.onAfterHardDestroy?.();\n Object.keys(this.options).forEach(o => delete (this as any)[o]);\n }\n }\n }\n\n protected initLocale() {\n if (this.options.locale) {\n if (typeof this.options.locale === 'object') {\n Object.assign(this.options, this.options.locale);\n return;\n }\n\n const locales = window.multipleSelect.locales;\n const parts = this.options.locale.split(/-|_/);\n\n parts[0] = parts[0].toLowerCase();\n if (parts[1]) {\n parts[1] = parts[1].toUpperCase();\n }\n\n if (locales[this.options.locale]) {\n Object.assign(this.options, locales[this.options.locale]);\n } else if (locales[parts.join('-')]) {\n Object.assign(this.options, locales[parts.join('-')]);\n } else if (locales[parts[0]]) {\n Object.assign(this.options, locales[parts[0]]);\n } else {\n throw new Error(`[multiple-select-vanilla] invalid locales \"${this.options.locale}\", make sure to import it before using it`);\n }\n }\n }\n\n protected initContainer() {\n const name = this.elm.getAttribute('name') || this.options.name || '';\n\n if (this.options.classes) {\n this.elm.classList.add(this.options.classes);\n }\n if (this.options.classPrefix) {\n this.elm.classList.add(this.options.classPrefix);\n\n if (this.options.size) {\n this.elm.classList.add(`${this.options.classPrefix}-${this.options.size}`);\n }\n }\n\n // hide select element\n this.elm.style.display = 'none';\n\n // label element\n this.labelElm = this.elm.closest('label');\n if (!this.labelElm && this.elm.id) {\n this.labelElm = document.createElement('label');\n this.labelElm.htmlFor = this.elm.id;\n }\n if (this.labelElm?.querySelector('input')) {\n this.labelElm = null;\n }\n\n // single or multiple\n if (typeof this.options.single === 'undefined') {\n this.options.single = !this.elm.multiple;\n }\n\n // restore class and title from select element\n this.parentElm = createDomElement('div', {\n className: classNameToList(`ms-parent ${this.elm.className || ''} ${this.options.classes}`).join(' '),\n dataset: { test: 'sel' },\n });\n\n if (this.options.darkMode) {\n this.parentElm.classList.add('ms-dark-mode');\n }\n\n // add tooltip title only when provided\n const parentTitle = this.elm.getAttribute('title') || '';\n if (parentTitle) {\n this.parentElm.title = parentTitle;\n }\n\n // add placeholder to choice button\n this.options.placeholder = this.options.placeholder || this.elm.getAttribute('placeholder') || '';\n\n this.choiceElm = createDomElement('button', { className: 'ms-choice', type: 'button' }, this.parentElm);\n\n if (this.options.labelId) {\n this.choiceElm.id = this.options.labelId;\n this.choiceElm.setAttribute('aria-labelledby', this.options.labelId);\n }\n\n this.choiceElm.appendChild(createDomElement('span', { className: 'ms-placeholder', textContent: this.options.placeholder }));\n\n if (this.options.showClear) {\n this.selectClearElm = createDomElement('div', { className: 'ms-icon ms-icon-close' });\n this.selectClearElm.style.display = 'none'; // don't show unless filled\n this.choiceElm.appendChild(this.selectClearElm);\n }\n\n this.choiceElm.appendChild(createDomElement('div', { className: 'ms-icon ms-icon-caret' }));\n\n // default position is bottom\n this.dropElm = createDomElement('div', { className: `ms-drop ${this.options.position}`, ariaExpanded: 'false' }, this.parentElm);\n\n if (this.options.darkMode) {\n this.dropElm.classList.add('ms-dark-mode');\n }\n\n // add data-name attribute when name option is defined\n if (name) {\n this.dropElm.dataset.name = name;\n }\n\n // add [data-test=\"name\"] attribute to both ms-parent & ms-drop\n const dataTest = this.elm.getAttribute('data-test') || this.options.dataTest;\n if (dataTest) {\n this.parentElm.dataset.test = dataTest;\n this.dropElm.dataset.test = dataTest;\n }\n\n this.closeElm = this.choiceElm.querySelector('.ms-icon-close');\n\n if (this.options.dropWidth) {\n this.dropElm.style.width = typeof this.options.dropWidth === 'string' ? this.options.dropWidth : `${this.options.dropWidth}px`;\n }\n\n insertAfter(this.elm, this.parentElm);\n\n if (this.elm.disabled) {\n this.choiceElm.classList.add('disabled');\n this.choiceElm.disabled = true;\n }\n\n this.selectAllName = `selectAll${name}`;\n this.selectGroupName = `selectGroup${name}`;\n this.selectItemName = `selectItem${name}`;\n\n if (!this.options.keepOpen) {\n this._bindEventService.unbindAll('body-click');\n this._bindEventService.bind(\n document.body,\n 'click',\n ((e: MouseEvent & { target: HTMLElement }) => {\n if (this.getEventTarget(e) === this.choiceElm || findParent(this.getEventTarget(e), '.ms-choice') === this.choiceElm) {\n return;\n }\n\n if (\n (this.getEventTarget(e) === this.dropElm ||\n (findParent(this.getEventTarget(e), '.ms-drop') !== this.dropElm && this.getEventTarget(e) !== this.elm)) &&\n this.options.isOpen\n ) {\n this.close('body.click');\n }\n }) as EventListener,\n undefined,\n 'body-click',\n );\n }\n }\n\n protected initData() {\n const data: Array<OptionRowData> = [];\n\n if (this.options.data) {\n if (Array.isArray(this.options.data)) {\n this.data = this.options.data.map((it: any) => {\n if (typeof it === 'string' || typeof it === 'number') {\n return {\n text: it,\n value: it,\n };\n }\n return it;\n });\n } else if (typeof this.options.data === 'object') {\n for (const [value, text] of Object.entries(this.options.data as OptionDataObject)) {\n data.push({\n value,\n text: `${text}`,\n });\n }\n this.data = data;\n }\n } else {\n this.elm.childNodes.forEach(elm => {\n const row = this.initRow(elm as HTMLOptionElement);\n if (row) {\n data.push(row as OptionRowData);\n }\n });\n\n this.options.data = data;\n this.data = data;\n this.fromHtml = true;\n }\n\n this.dataTotal = setDataKeys(this.data || []);\n }\n\n protected initRow(elm: HTMLOptionElement, groupDisabled?: boolean) {\n const row = {} as OptionRowData | OptGroupRowData;\n if (elm.tagName?.toLowerCase() === 'option') {\n row.type = 'option';\n (row as OptionRowData).text = this.options.textTemplate(elm);\n row.value = elm.value;\n row.visible = true;\n row.selected = !!elm.selected;\n row.disabled = groupDisabled || elm.disabled;\n row.classes = elm.getAttribute('class') || '';\n row.title = elm.getAttribute('title') || '';\n\n if (elm.dataset.value) {\n row._value = elm.dataset.value; // value for object\n }\n if (Object.keys(elm.dataset).length) {\n row._data = elm.dataset;\n\n if (row._data.divider) {\n row.divider = row._data.divider;\n }\n }\n\n return row;\n }\n\n if (elm.tagName?.toLowerCase() === 'optgroup') {\n row.type = 'optgroup';\n (row as OptGroupRowData).label = this.options.labelTemplate(elm);\n row.visible = true;\n row.selected = !!elm.selected;\n row.disabled = elm.disabled;\n (row as OptGroupRowData).children = [];\n if (Object.keys(elm.dataset).length) {\n row._data = elm.dataset;\n }\n\n elm.childNodes.forEach(childNode => {\n (row as OptGroupRowData).children.push(this.initRow(childNode as HTMLOptionElement, row.disabled) as OptionRowData);\n });\n\n return row;\n }\n\n return null;\n }\n\n protected initDrop() {\n this.initList();\n this.update(true);\n\n if (this.options.isOpen) {\n this.open(10);\n }\n\n if (this.options.openOnHover && this.parentElm) {\n this._bindEventService.bind(this.parentElm, 'mouseover', () => this.open(null));\n this._bindEventService.bind(this.parentElm, 'mouseout', () => this.close('hover.mouseout'));\n }\n }\n\n protected initFilter() {\n this.filterText = '';\n\n if (this.options.filter || !this.options.filterByDataLength) {\n return;\n }\n\n let length = 0;\n for (const option of this.data || []) {\n if ((option as OptGroupRowData).type === 'optgroup') {\n length += (option as OptGroupRowData).children.length;\n } else {\n length += 1;\n }\n }\n this.options.filter = length > this.options.filterByDataLength;\n }\n\n protected initList() {\n if (this.options.filter) {\n this.filterParentElm = createDomElement('div', { className: 'ms-search' }, this.dropElm);\n this.filterParentElm.appendChild(\n createDomElement('input', {\n autocomplete: 'off',\n autocapitalize: 'off',\n spellcheck: false,\n type: 'text',\n placeholder: this.options.filterPlaceholder || '\uD83D\uDD0E\uFE0E',\n }),\n );\n\n if (this.options.showSearchClear) {\n this.filterParentElm.appendChild(createDomElement('span', { className: 'ms-icon ms-icon-close' }));\n }\n }\n\n if (this.options.selectAll && !this.options.single) {\n const selectName = this.elm.getAttribute('name') || this.options.name || '';\n this.selectAllParentElm = createDomElement('div', { className: 'ms-select-all', dataset: { key: 'select_all' } });\n const saLabelElm = document.createElement('label');\n const saIconClass = this.isAllSelected ? 'ms-icon-check' : this.isPartiallyAllSelected ? 'ms-icon-minus' : 'ms-icon-uncheck';\n const selectAllIconClass = `ms-icon ${saIconClass}`;\n const saIconContainerElm = createDomElement('div', { className: 'icon-checkbox-container' }, saLabelElm);\n createDomElement(\n 'input',\n {\n type: 'checkbox',\n ariaChecked: String(this.isAllSelected),\n checked: this.isAllSelected,\n dataset: { name: `selectAll${selectName}` },\n },\n saIconContainerElm,\n );\n createDomElement('div', { className: selectAllIconClass }, saIconContainerElm);\n\n saLabelElm.appendChild(createDomElement('span', { textContent: this.formatSelectAll() }));\n this.selectAllParentElm.appendChild(saLabelElm);\n this.dropElm?.appendChild(this.selectAllParentElm);\n }\n\n this.ulElm = document.createElement('ul');\n this.ulElm.role = 'combobox';\n this.ulElm.ariaExpanded = 'false';\n this.ulElm.ariaMultiSelectable = String(!this.options.single);\n this.dropElm?.appendChild(this.ulElm);\n\n if (this.options.showOkButton && !this.options.single) {\n this.okButtonElm = createDomElement(\n 'button',\n { className: 'ms-ok-button', type: 'button', textContent: this.formatOkButton() },\n this.dropElm,\n );\n }\n this.initListItems();\n }\n\n protected initListItems(): HtmlStruct[] {\n let offset = 0;\n const rows = this.getListRows();\n\n if (this.options.selectAll && !this.options.single) {\n offset = -1;\n }\n\n if (rows.length > Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS) {\n const dropVisible = this.dropElm && this.dropElm?.style.display !== 'none';\n if (!dropVisible && this.dropElm) {\n this.dropElm.style.left = '-10000';\n this.dropElm.style.display = 'block';\n this.dropElm.ariaExpanded = 'true';\n }\n\n const updateDataOffset = () => {\n if (this.virtualScroll) {\n this._currentHighlightIndex = 0;\n this.updateDataStart = this.virtualScroll.dataStart + offset;\n this.updateDataEnd = this.virtualScroll.dataEnd + offset;\n\n if (this.updateDataStart < 0) {\n this.updateDataStart = 0;\n this._currentHighlightIndex = 0;\n }\n const dataLn = this.getDataLength();\n if (this.updateDataEnd > dataLn) {\n this.updateDataEnd = dataLn;\n }\n\n if (this.ulElm) {\n if (this.isMoveUpRecalcRequired) {\n this.recalculateArrowMove('up');\n } else if (this.virtualScroll.dataStart > this.updateDataStart) {\n this.recalculateArrowMove('down');\n }\n }\n }\n };\n\n if (this.ulElm) {\n if (!this.virtualScroll) {\n this.virtualScroll = new VirtualScroll({\n rows,\n scrollEl: this.ulElm,\n contentEl: this.ulElm,\n sanitizer: this.options.sanitizer,\n callback: () => {\n updateDataOffset();\n this.events();\n },\n });\n } else {\n this.virtualScroll.reset(rows);\n }\n }\n updateDataOffset();\n\n if (!dropVisible && this.dropElm) {\n this.dropElm.style.left = '0';\n this.dropElm.style.display = 'none';\n this.dropElm.ariaExpanded = 'false';\n }\n } else {\n if (this.ulElm) {\n emptyElement(this.ulElm);\n rows.forEach(itemRow => this.ulElm!.appendChild(convertItemRowToHtml(itemRow)));\n }\n this.updateDataStart = 0;\n this.updateDataEnd = this.updateData.length;\n }\n\n this.events();\n\n return rows;\n }\n\n protected getEventTarget(e: Event & { target: HTMLElement }): HTMLElement {\n if (e.composedPath) {\n return e.composedPath()[0] as HTMLElement;\n }\n return e.target as HTMLElement;\n }\n\n protected getListRows(): HtmlStruct[] {\n const rows: HtmlStruct[] = [];\n this.updateData = [];\n this.data?.forEach(dataRow => rows.push(...this.initListItem(dataRow)));\n\n // when infinite scroll is enabled, we'll add an empty <li> element (that will never be clickable)\n // so that scrolling to the last valid item will NOT automatically scroll back to the top of the list.\n // However scrolling by 1 more item (the last invisible item) will at that time trigger the scroll back to the top of the list\n if (this.options.infiniteScroll) {\n rows.push({\n tagName: 'li',\n props: { className: 'ms-infinite-option', role: 'option' },\n });\n }\n\n // add a \"No Results\" option that is hidden by default\n rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound() } });\n\n return rows;\n }\n\n protected initListItem(dataRow: OptionRowData | OptGroupRowData, level = 0): HtmlStruct[] {\n const title = dataRow?.title || '';\n const multiple = this.options.multiple ? 'multiple' : '';\n const type = this.options.single ? 'radio' : 'checkbox';\n const isChecked = !!dataRow?.selected;\n const isSingleWithoutRadioIcon = this.options.single && !this.options.singleRadio;\n let classes = '';\n\n if (!dataRow?.visible) {\n return [];\n }\n\n this.updateData.push(dataRow);\n\n if (isSingleWithoutRadioIcon) {\n classes = 'hide-radio ';\n }\n\n if (dataRow.selected) {\n classes += 'selected ';\n }\n\n if (dataRow.type === 'optgroup') {\n // - group option row -\n const htmlBlocks: HtmlStruct[] = [];\n\n let itemOrGroupBlock: HtmlStruct;\