diff --git a/demo/src/app-routing.ts b/demo/src/app-routing.ts index 4a93b80dc..1cbd9478d 100644 --- a/demo/src/app-routing.ts +++ b/demo/src/app-routing.ts @@ -11,6 +11,7 @@ import Example09 from './examples/example09'; import Example10 from './examples/example10'; import Example11 from './examples/example11'; import Example12 from './examples/example12'; +import Example13 from './examples/example13'; import Options01 from './options/options01'; import Options02 from './options/options02'; import Options03 from './options/options03'; @@ -78,6 +79,7 @@ export const exampleRouting = [ { name: 'example10', view: '/src/examples/example10.html', viewModel: Example10, title: 'Large Select' }, { name: 'example11', view: '/src/examples/example11.html', viewModel: Example11, title: 'The Themes' }, { name: 'example12', view: '/src/examples/example12.html', viewModel: Example12, title: 'Checkbox/Radio Icons' }, + { name: 'example13', view: '/src/examples/example13.html', viewModel: Example13, title: 'Dynamically Create Select' }, ], }, { diff --git a/demo/src/examples/example13.html b/demo/src/examples/example13.html new file mode 100644 index 000000000..9f8f88c6b --- /dev/null +++ b/demo/src/examples/example13.html @@ -0,0 +1,44 @@ +
+
+

+ Dynamically create Multiple-Select with Data collection + + Code + + html + | + ts + + +

+
+ Dynamically create a Multiple-Select instance with data property. +
+
+
+
+ +
+
+ + +
+ + +
+
+ +
+ + +
+ +
+
+
diff --git a/demo/src/examples/example13.ts b/demo/src/examples/example13.ts new file mode 100644 index 000000000..56158ba5b --- /dev/null +++ b/demo/src/examples/example13.ts @@ -0,0 +1,87 @@ +import { multipleSelect, MultipleSelectInstance } from 'multiple-select-vanilla'; + +export default class Example { + createBtnElm?: HTMLButtonElement | null; + destroyBtnElm?: HTMLButtonElement | null; + ms1?: MultipleSelectInstance; + + mount() { + this.createBtnElm = document.querySelector('#createBtn'); + this.destroyBtnElm = document.querySelector('#destroyBtn'); + this.createBtnElm!.addEventListener('click', this.createMultipleSelect.bind(this)); + this.destroyBtnElm!.addEventListener('click', this.destroyMultiSelect.bind(this)); + } + + createMultipleSelect() { + this.ms1 = multipleSelect('#select1', { + name: 'my-select', + single: false, + useSelectOptionLabelToHtml: true, + data: [ + { + text: ' January', + value: 1, + }, + { + text: 'February', + value: 2, + }, + { + text: 'March', + value: 3, + }, + { + text: 'April', + value: 4, + }, + { + text: 'May', + value: 5, + }, + { + text: 'June', + value: 6, + }, + { + text: 'July', + value: 7, + }, + { + text: 'August', + value: 8, + }, + { + text: 'September', + value: 9, + }, + { + text: 'October', + value: 10, + }, + { + text: 'November', + value: 11, + }, + { + text: 'December', + value: 12, + }, + ], + }) as MultipleSelectInstance; + + this.ms1.setSelects([1, 3, 4]); + } + + destroyMultiSelect() { + console.log('destroy'); + this.ms1?.destroy(); + this.ms1 = undefined; // remove detached element + } + + unmount() { + // destroy ms instance(s) to avoid DOM leaks + this.destroyMultiSelect(); + this.createBtnElm!.removeEventListener('click', this.createMultipleSelect.bind(this)); + this.destroyBtnElm!.removeEventListener('click', this.destroyMultiSelect.bind(this)); + } +} diff --git a/demo/src/methods/methods11.ts b/demo/src/methods/methods11.ts index 4b7bb7eb7..ae53957a7 100644 --- a/demo/src/methods/methods11.ts +++ b/demo/src/methods/methods11.ts @@ -1,24 +1,32 @@ import { MultipleSelectInstance, multipleSelect } from 'multiple-select-vanilla'; export default class Example { + buildBtnElm?: HTMLButtonElement | null; + destroyBtnElm?: HTMLButtonElement | null; ms1?: MultipleSelectInstance | null; mount() { + this.buildBtnElm = document.querySelector('#buildBtn'); + this.destroyBtnElm = document.querySelector('#destroyBtn'); + this.destroyBtnElm!.addEventListener('click', this.destroyMultiSelect.bind(this)); + this.buildBtnElm!.addEventListener('click', this.createMultipleSelect.bind(this)); + this.ms1 = multipleSelect('select') as MultipleSelectInstance | null; + } - document.querySelector('#destroyBtn')!.addEventListener('click', () => { - this.ms1?.destroy(); - this.ms1 = null; // remove detached element - }); + createMultipleSelect() { + this.ms1 = multipleSelect('select') as MultipleSelectInstance; + } - document.querySelector('#buildBtn')!.addEventListener('click', () => { - this.ms1 = multipleSelect('select') as MultipleSelectInstance; - }); + destroyMultiSelect() { + this.ms1?.destroy(); + this.ms1 = null; // remove detached element } unmount() { // destroy ms instance(s) to avoid DOM leaks - this.ms1?.destroy(); - this.ms1 = undefined; + this.destroyMultiSelect(); + this.buildBtnElm!.removeEventListener('click', this.destroyMultiSelect.bind(this)); + this.destroyBtnElm!.removeEventListener('click', this.createMultipleSelect.bind(this)); } } diff --git a/demo/src/options/options17.html b/demo/src/options/options17.html index 4166d8627..a1810451b 100644 --- a/demo/src/options/options17.html +++ b/demo/src/options/options17.html @@ -92,4 +92,27 @@

+ +
+ + +
+
+ +
+
+
diff --git a/demo/src/options/options17.ts b/demo/src/options/options17.ts index 1260ee7e0..3437678ba 100644 --- a/demo/src/options/options17.ts +++ b/demo/src/options/options17.ts @@ -4,11 +4,13 @@ export default class Example { ms1?: MultipleSelectInstance; ms2?: MultipleSelectInstance; ms3?: MultipleSelectInstance; + ms4?: MultipleSelectInstance; mount() { this.ms1 = multipleSelect('.select1') as MultipleSelectInstance; this.ms2 = multipleSelect('.select2') as MultipleSelectInstance; this.ms3 = multipleSelect('.select3', { container: '.my-container' }) as MultipleSelectInstance; + this.ms4 = multipleSelect('.select4', { autoAdjustDropPosition: true, container: 'body' }) as MultipleSelectInstance; } unmount() { @@ -16,8 +18,10 @@ export default class Example { this.ms1?.destroy(); this.ms2?.destroy(); this.ms3?.destroy(); + this.ms4?.destroy(); this.ms1 = undefined; this.ms2 = undefined; this.ms3 = undefined; + this.ms4 = undefined; } } diff --git a/lib/package.json b/lib/package.json index 5b32e9709..b01838a5a 100644 --- a/lib/package.json +++ b/lib/package.json @@ -8,7 +8,8 @@ ".": { "import": "./dist/esm/multiple-select.js", "require": "./dist/cjs/multiple-select.js", - "default": "./dist/esm/multiple-select.js" + "default": "./dist/esm/multiple-select.js", + "types": "./dist/types/index.d.ts" }, "./*": "./*" }, diff --git a/lib/src/MultipleSelectInstance.ts b/lib/src/MultipleSelectInstance.ts index 5e269d41b..43c963245 100644 --- a/lib/src/MultipleSelectInstance.ts +++ b/lib/src/MultipleSelectInstance.ts @@ -58,8 +58,8 @@ export class MultipleSelectInstance { this._bindEventService = new BindingEventService({ distinctEvent: true }); } - async init() { - await this.initLocale(); + init() { + this.initLocale(); this.initContainer(); this.initData(); this.initSelected(true); @@ -88,6 +88,7 @@ export class MultipleSelectInstance { } this.virtualScroll?.destroy(); + this.dropElm?.remove(); this.parentElm.parentNode?.removeChild(this.parentElm); if (this.fromHtml) { @@ -104,7 +105,7 @@ export class MultipleSelectInstance { } } - protected async initLocale() { + protected initLocale() { if (this.options.locale) { const locales = window.multipleSelect.locales; const parts = this.options.locale.split(/-|_/); @@ -152,9 +153,14 @@ export class MultipleSelectInstance { // restore class and title from select element this.parentElm = createDomElement('div', { className: `ms-parent ${this.elm.className || ''}`, - title: this.elm.getAttribute('title') || '', }); + // add tooltip title only when provided + const parentTitle = this.elm.getAttribute('title') || ''; + if (parentTitle) { + this.parentElm.title = parentTitle; + } + // add placeholder to choice button this.options.placeholder = this.options.placeholder || this.elm.getAttribute('placeholder') || ''; @@ -200,6 +206,11 @@ export class MultipleSelectInstance { className: `ms-drop ${this.options.position}`, }); + // add name attribute when defined + if (name) { + this.dropElm.setAttribute('name', name); + } + this.closeElm = this.choiceElm.querySelector('.icon-close'); if (this.options.dropWidth) { @@ -328,7 +339,7 @@ export class MultipleSelectInstance { this.update(true); if (this.options.isOpen) { - setTimeout(() => this.open(), 50); + setTimeout(() => this.open(), 10); } if (this.options.openOnHover && this.parentElm) { @@ -604,7 +615,7 @@ export class MultipleSelectInstance { } this.parentElm.style.width = `${this.options.width || computedWidth}px`; - this.elm.style.display = 'block'; + // this.elm.style.display = 'inline-block'; this.elm.classList.add('ms-offscreen'); } @@ -817,6 +828,7 @@ export class MultipleSelectInstance { this.dropElm.style.width = `${getElementSize(this.parentElm, 'outer', 'width')}px`; } + let minHeight = this.options.minHeight; let maxHeight = this.options.maxHeight; if (this.options.maxHeightUnit === 'row') { const liElm = this.dropElm.querySelector('ul>li'); @@ -824,6 +836,9 @@ export class MultipleSelectInstance { } const ulElm = this.dropElm.querySelector('ul'); if (ulElm) { + if (minHeight) { + ulElm.style.minHeight = `${minHeight}px`; + } ulElm.style.maxHeight = `${maxHeight}px`; } const multElms = this.dropElm.querySelectorAll('.multiple'); @@ -890,7 +905,7 @@ export class MultipleSelectInstance { const getSelectOptionHtml = () => { if (this.options.useSelectOptionLabel || this.options.useSelectOptionLabelToHtml) { - const labels = valueSelects.join(this.options.delimiter); + const labels = valueSelects.join(this.options.displayDelimiter); return this.options.useSelectOptionLabelToHtml ? stripScripts(labels) : labels; } else { return textSelects.join(this.options.displayDelimiter); @@ -928,7 +943,7 @@ export class MultipleSelectInstance { console.warn('[Multiple-Select-Vanilla] Please note that the `addTitle` option was replaced with `displayTitle`.'); } const selectType = this.options.useSelectOptionLabel || this.options.useSelectOptionLabelToHtml ? 'value' : 'text'; - spanElm.title = this.getSelects(selectType).join(''); + spanElm.title = this.getSelects(selectType).join(this.options.displayDelimiter); } } @@ -1005,6 +1020,14 @@ export class MultipleSelectInstance { this.init(); } + getDropElement() { + return this.dropElm; + } + + getParentElement() { + return this.parentElm; + } + // value html, or text, default: 'value' getSelects(type = 'value') { const values = []; @@ -1319,7 +1342,7 @@ export class MultipleSelectInstance { } protected adjustDropWidthByText() { - const parentWidth = this.parentElm.clientWidth; + const parentWidth = this.parentElm.scrollWidth; // keep the dropWidth/width as reference, if our new calculated width is below then we will re-adjust (else do nothing) let currentDefinedWidth: number | string = parentWidth; @@ -1328,16 +1351,12 @@ export class MultipleSelectInstance { } // calculate the "Select All" element width, this text is configurable which is why we recalculate every time - const selectAllSpanElm = this.dropElm.querySelector('.ms-select-all span') as HTMLSpanElement; + const selectAllSpanElm = this.dropElm.querySelector('.ms-select-all span'); const dropUlElm = this.dropElm.querySelector('ul') as HTMLUListElement; - let liPadding = 0; - const firstLiElm = this.dropElm.querySelector('li'); // get padding of 1st
  • element - if (firstLiElm) { - const { paddingLeft, paddingRight } = window.getComputedStyle(firstLiElm); - liPadding = parseFloat(paddingLeft) + parseFloat(paddingRight); - } - const selectAllElmWidth = selectAllSpanElm.clientWidth + liPadding; + let liPadding = 26; // there are multiple padding involved, let's fix it at 26px + + const selectAllElmWidth = selectAllSpanElm?.clientWidth ?? 0 + liPadding; const hasScrollbar = dropUlElm.scrollHeight > dropUlElm.clientHeight; const scrollbarWidth = hasScrollbar ? this.getScrollbarWidth() : 0; let contentWidth = 0; diff --git a/lib/src/interfaces/multipleSelectOption.interface.ts b/lib/src/interfaces/multipleSelectOption.interface.ts index cf568b788..97349ea66 100644 --- a/lib/src/interfaces/multipleSelectOption.interface.ts +++ b/lib/src/interfaces/multipleSelectOption.interface.ts @@ -28,9 +28,6 @@ export interface MultipleSelectOption extends MultipleSelectLocale { /** provide custom data */ data?: any | any[]; - /** Delimiter to use when display the selected options. By default this option is set to `,` */ - delimiter?: string; - /** Delimiter to be displayed between each option */ displayDelimiter: string; @@ -82,6 +79,9 @@ export interface MultipleSelectOption extends MultipleSelectLocale { /** maxHeight unit type */ maxHeightUnit?: string; + /** Defaults to 150, define the minimum height property of the dropdown list. */ + minHeight: number; + /** Defaults to 500, define the maximum width of the drop when using the "autoAdjustDropWidthByTextSize: true" flag. */ maxWidth?: number; diff --git a/lib/src/styles/_variables.scss b/lib/src/styles/_variables.scss index 36aec7eb5..a8e985956 100644 --- a/lib/src/styles/_variables.scss +++ b/lib/src/styles/_variables.scss @@ -29,6 +29,8 @@ $ms-drop-hide-radio-selected-bgcolor: #007bff !default; $ms-drop-input-margin-left: -1.25rem !default; $ms-drop-input-margin-top: 0.3rem !default; $ms-drop-optgroup-font-weight: bold !default; +$ms-drop-list-margin: 0px !default; +$ms-drop-list-padding: 0px !default; $ms-drop-list-item-level1-padding-left: 28px !default; $ms-drop-option-divider-padding: 0 !default; $ms-drop-option-divider-border-top: 1px solid #e9ecef !default; @@ -39,7 +41,7 @@ $ms-drop-list-item-disabled-filter: Alpha(Opacity = 35) !default; $ms-drop-list-item-disabled-opacity: 0.35 !default; $ms-drop-zindex: 1050 !default; $ms-label-margin-bottom: 0 !default; -$ms-label-padding-left: 1.25rem !default; +$ms-label-padding: 0 0 0 1.25rem !default; $ms-ok-button-bg-color: #fff !default; $ms-ok-button-bg-hover-color: #f9f9f9 !default; $ms-ok-button-border-color: #ccc !default; @@ -67,7 +69,7 @@ $ms-select-all-label-border: $ms-item-border !default; $ms-select-all-label-hover-border: 1px solid transparent !default; $ms-select-all-label-hover-bg-color: $ms-checkbox-hover-bg-color !default; $ms-select-all-label-padding: 4px !default; -$ms-select-all-label-span-padding-left: $ms-label-padding-left !default; +$ms-select-all-label-span-padding: 0 0 0 20px !default; $ms-select-all-line-height: 18px !default; $ms-select-all-padding: 4px !default; $ms-select-all-text-color: darken($primary-color, 5%) !default; diff --git a/lib/src/styles/multiple-select.scss b/lib/src/styles/multiple-select.scss index bd9063491..958b8303d 100644 --- a/lib/src/styles/multiple-select.scss +++ b/lib/src/styles/multiple-select.scss @@ -154,7 +154,7 @@ margin-left: 0; } span { - padding-left: var(--ms-select-all-label-span-padding-left, $ms-select-all-label-span-padding-left); + padding: var(--ms-select-all-label-span-padding, $ms-select-all-label-span-padding); } } } @@ -208,8 +208,8 @@ .ms-drop { ul { overflow: auto; - margin: 0; - padding: 0; + margin: var(--ms-drop-list-margin, $ms-drop-list-margin); + padding: var(--ms-drop-list-padding, $ms-drop-list-padding); > li { background-image: none; @@ -249,7 +249,7 @@ position: relative; white-space: nowrap; margin-bottom: var(--ms-label-margin-bottom, $ms-label-margin-bottom); - padding-left: var(--ms-label-padding-left, $ms-label-padding-left); + padding: var(--ms-label-padding, $ms-label-padding); &.optgroup { font-weight: var(--ms-drop-optgroup-font-weight, $ms-drop-optgroup-font-weight);