import Constants from './constants/index.js' import VirtualScroll from './virtual-scroll/index.js' import { compareObjects, removeDiacritics, findByParam, setDataKeys, removeUndefined, getDocumentClickEvent } from './utils/index.js' class MultipleSelect { constructor ($el, options) { this.$el = $el this.options = $.extend({}, Constants.DEFAULTS, options) } init () { this.initLocale() this.initContainer() this.initData() this.initSelected(true) this.initFilter() this.initDrop() this.initView() this.options.onAfterCreate() } initLocale () { if (this.options.locale) { const {locales} = $.fn.multipleSelect const parts = this.options.locale.split(/-|_/) parts[0] = parts[0].toLowerCase() if (parts[1]) { parts[1] = parts[1].toUpperCase() } if (locales[this.options.locale]) { $.extend(this.options, locales[this.options.locale]) } else if (locales[parts.join('-')]) { $.extend(this.options, locales[parts.join('-')]) } else if (locales[parts[0]]) { $.extend(this.options, locales[parts[0]]) } } } initContainer () { const el = this.$el[0] const name = el.getAttribute('name') || this.options.name || '' // hide select element this.$el.hide() // label element this.$label = this.$el.closest('label') if (!this.$label.length && this.$el.attr('id')) { this.$label = $(`label[for="${this.$el.attr('id')}"]`) } if (this.$label.find('>input').length) { this.$label = null } // single or multiple if (typeof this.options.single === 'undefined') { this.options.single = el.getAttribute('multiple') === null } // restore class and title from select element this.$parent = $(`
`) // add placeholder to choice button this.options.placeholder = this.options.placeholder || el.getAttribute('placeholder') || '' this.tabIndex = el.getAttribute('tabindex') let tabIndex = '' if (this.tabIndex !== null) { this.$el.attr('tabindex', -1) tabIndex = this.tabIndex && `tabindex="${this.tabIndex}"` } this.$choice = $(` `) // default position is bottom this.$drop = $(`
`) this.$close = this.$choice.find('.icon-close') if (this.options.dropWidth) { this.$drop.css('width', this.options.dropWidth) } this.$el.after(this.$parent) this.$parent.append(this.$choice) this.$parent.append(this.$drop) if (el.disabled) { this.$choice.addClass('disabled') } this.selectAllName = `data-name="selectAll${name}"` this.selectGroupName = `data-name="selectGroup${name}"` this.selectItemName = `data-name="selectItem${name}"` if (!this.options.keepOpen) { const clickEvent = getDocumentClickEvent(this.$el.attr('id')) $(document).off(clickEvent).on(clickEvent, e => { if ( $(e.target)[0] === this.$choice[0] || $(e.target).parents('.ms-choice')[0] === this.$choice[0] ) { return } if ( ($(e.target)[0] === this.$drop[0] || ($(e.target).parents('.ms-drop')[0] !== this.$drop[0] && e.target !== el)) && this.options.isOpen ) { this.close() } }) } } initData () { const data = [] if (this.options.data) { if (Array.isArray(this.options.data)) { this.data = this.options.data.map(it => { if (typeof it === 'string' || typeof it === 'number') { return { text: it, value: it } } return it }) } else if (typeof this.options.data === 'object') { for (const [value, text] of Object.entries(this.options.data)) { data.push({ value, text }) } this.data = data } } else { $.each(this.$el.children(), (i, elm) => { const row = this.initRow(i, elm) if (row) { data.push(this.initRow(i, elm)) } }) this.options.data = data this.data = data this.fromHtml = true } this.dataTotal = setDataKeys(this.data) } initRow (i, elm, groupDisabled) { const row = {} const $elm = $(elm) if ($elm.is('option')) { row.type = 'option' row.text = this.options.textTemplate($elm) row.value = elm.value row.visible = true row.selected = !!elm.selected row.disabled = groupDisabled || elm.disabled row.classes = elm.getAttribute('class') || '' row.title = elm.getAttribute('title') || '' if ($elm.data('value')) { row._value = $elm.data('value') // value for object } if (Object.keys($elm.data()).length) { row._data = $elm.data() } return row } if ($elm.is('optgroup')) { row.type = 'optgroup' row.label = this.options.labelTemplate($elm) row.visible = true row.selected = !!elm.selected row.disabled = elm.disabled row.children = [] if (Object.keys($elm.data()).length) { row._data = $elm.data() } $.each($elm.children(), (j, elem) => { row.children.push(this.initRow(j, elem, row.disabled)) }) return row } return null } initSelected (ignoreTrigger) { let selectedTotal = 0 for (const row of this.data) { if (row.type === 'optgroup') { const selectedCount = row.children.filter(child => { return child.selected && !child.disabled && child.visible }).length row.selected = selectedCount && selectedCount === row.children.filter(child => !child.disabled && child.visible).length selectedTotal += selectedCount } else { selectedTotal += row.selected && !row.disabled && row.visible ? 1 : 0 } } this.allSelected = this.data.filter(row => { return row.selected && !row.disabled && row.visible }).length === this.data.filter(row => !row.disabled && row.visible).length if (!ignoreTrigger) { if (this.allSelected) { this.options.onCheckAll() } else if (selectedTotal === 0) { this.options.onUncheckAll() } } } initFilter () { this.filterText = '' if (this.options.filter || !this.options.filterByDataLength) { return } let length = 0 for (const option of this.data) { if (option.type === 'optgroup') { length += option.children.length } else { length += 1 } } this.options.filter = length > this.options.filterByDataLength } initDrop () { this.initList() this.update(true) if (this.options.isOpen) { setTimeout(() => { this.open() }, 50) } if (this.options.openOnHover) { this.$parent.hover(() => { this.open() }, () => { this.close() }) } } initList () { const html = [] if (this.options.filter) { html.push(` `) } html.push('') this.$drop.html(html.join('')) this.$ul = this.$drop.find('>ul') this.initListItems() } initListItems () { const rows = this.getListRows() let offset = 0 if (this.options.selectAll && !this.options.single) { offset = -1 } if (rows.length > Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS) { if (this.virtualScroll) { this.virtualScroll.destroy() } const dropVisible = this.$drop.is(':visible') if (!dropVisible) { this.$drop.css('left', -10000).show() } const updateDataOffset = () => { this.updateDataStart = this.virtualScroll.dataStart + offset this.updateDataEnd = this.virtualScroll.dataEnd + offset if (this.updateDataStart < 0) { this.updateDataStart = 0 } if (this.updateDataEnd > this.data.length) { this.updateDataEnd = this.data.length } } this.virtualScroll = new VirtualScroll({ rows, scrollEl: this.$ul[0], contentEl: this.$ul[0], callback: () => { updateDataOffset() this.events() } }) updateDataOffset() if (!dropVisible) { this.$drop.css('left', 0).hide() } } else { this.$ul.html(rows.join('')) this.updateDataStart = 0 this.updateDataEnd = this.updateData.length this.virtualScroll = null } this.events() } getListRows () { const rows = [] if (this.options.selectAll && !this.options.single) { rows.push(`
  • `) } this.updateData = [] this.data.forEach(row => { rows.push(...this.initListItem(row)) }) rows.push(`
  • ${this.options.formatNoMatchesFound()}
  • `) return rows } initListItem (row, level = 0) { const title = row.title ? `title="${row.title}"` : '' const multiple = this.options.multiple ? 'multiple' : '' const type = this.options.single ? 'radio' : 'checkbox' let classes = '' if (!row.visible) { return [] } this.updateData.push(row) if (this.options.single && !this.options.singleRadio) { classes = 'hide-radio ' } if (row.selected) { classes += 'selected ' } if (row.type === 'optgroup') { const customStyle = this.options.styler(row) const style = customStyle ? `style="${customStyle}"` : '' const html = [] const group = this.options.hideOptgroupCheckboxes || this.options.single ? `` : `` if ( !classes.includes('hide-radio') && (this.options.hideOptgroupCheckboxes || this.options.single) ) { classes += 'hide-radio ' } html.push(`
  • `) row.children.forEach(child => { html.push(...this.initListItem(child, 1)) }) return html } const customStyle = this.options.styler(row) const style = customStyle ? `style="${customStyle}"` : '' classes += row.classes || '' if (level && this.options.single) { classes += `option-level-${level} ` } return [`
  • `] } events () { this.$searchInput = this.$drop.find('.ms-search input') this.$selectAll = this.$drop.find(`input[${this.selectAllName}]`) this.$selectGroups = this.$drop.find(`input[${this.selectGroupName}],span[${this.selectGroupName}]`) this.$selectItems = this.$drop.find(`input[${this.selectItemName}]:enabled`) this.$disableItems = this.$drop.find(`input[${this.selectItemName}]:disabled`) this.$noResults = this.$drop.find('.ms-no-results') const toggleOpen = e => { e.preventDefault() if ($(e.target).hasClass('icon-close')) { return } this[this.options.isOpen ? 'close' : 'open']() } if (this.$label && this.$label.length) { this.$label.off('click').on('click', e => { if (e.target.nodeName.toLowerCase() !== 'label') { return } toggleOpen(e) if (!this.options.filter || !this.options.isOpen) { this.focus() } e.stopPropagation() // Causes lost focus otherwise }) } this.$choice.off('click').on('click', toggleOpen) .off('focus').on('focus', this.options.onFocus) .off('blur').on('blur', this.options.onBlur) this.$parent.off('keydown').on('keydown', e => { // esc key if (e.which === 27 && !this.options.keepOpen) { this.close() this.$choice.focus() } }) this.$close.off('click').on('click', e => { e.preventDefault() this._checkAll(false, true) this.initSelected(false) this.updateSelected() this.update() this.options.onClear() }) this.$searchInput.off('keydown').on('keydown', e => { // Ensure shift-tab causes lost focus from filter as with clicking away if (e.keyCode === 9 && e.shiftKey) { this.close() } }).off('keyup').on('keyup', e => { // enter or space // Avoid selecting/deselecting if no choices made if ( this.options.filterAcceptOnEnter && [13, 32].includes(e.which) && this.$searchInput.val() ) { if (this.options.single) { const $items = this.$selectItems.closest('li').filter(':visible') if ($items.length) { this.setSelects([$items.first().find(`input[${this.selectItemName}]`).val()]) } } else { this.$selectAll.click() } this.close() this.focus() return } this.filter() }) this.$selectAll.off('click').on('click', e => { this._checkAll($(e.currentTarget).prop('checked')) }) this.$selectGroups.off('click').on('click', e => { const $this = $(e.currentTarget) const checked = $this.prop('checked') const group = findByParam(this.data, '_key', $this.data('key')) this._checkGroup(group, checked) this.options.onOptgroupClick(removeUndefined({ label: group.label, selected: group.selected, data: group._data, children: group.children.map(child => { return removeUndefined({ text: child.text, value: child.value, selected: child.selected, disabled: child.disabled, data: child._data }) }) })) }) this.$selectItems.off('click').on('click', e => { const $this = $(e.currentTarget) const checked = $this.prop('checked') const option = findByParam(this.data, '_key', $this.data('key')) this._check(option, checked) this.options.onClick(removeUndefined({ text: option.text, value: option.value, selected: option.selected, data: option._data })) if (this.options.single && this.options.isOpen && !this.options.keepOpen) { this.close() } }) } initView () { let computedWidth if (window.getComputedStyle) { computedWidth = window.getComputedStyle(this.$el[0]).width if (computedWidth === 'auto') { computedWidth = this.$drop.outerWidth() + 20 } } else { computedWidth = this.$el.outerWidth() + 20 } this.$parent.css('width', this.options.width || computedWidth) this.$el.show().addClass('ms-offscreen') } open () { if (this.$choice.hasClass('disabled')) { return } this.options.isOpen = true this.$choice.find('>div').addClass('open') this.$drop[this.animateMethod('show')]() // fix filter bug: no results show this.$selectAll.parent().show() this.$noResults.hide() // Fix #77: 'All selected' when no options if (!this.data.length) { this.$selectAll.parent().hide() this.$noResults.show() } if (this.options.container) { const offset = this.$drop.offset() this.$drop.appendTo($(this.options.container)) this.$drop.offset({ top: offset.top, left: offset.left }) .css('min-width', 'auto') .outerWidth(this.$parent.outerWidth()) } let maxHeight = this.options.maxHeight if (this.options.maxHeightUnit === 'row') { maxHeight = this.$drop.find('>ul>li').first().outerHeight() * this.options.maxHeight } this.$drop.find('>ul').css('max-height', `${maxHeight}px`) this.$drop.find('.multiple').css('width', `${this.options.multipleWidth}px`) if (this.data.length && this.options.filter) { this.$searchInput.val('') this.$searchInput.focus() this.filter(true) } this.options.onOpen() } close () { this.options.isOpen = false this.$choice.find('>div').removeClass('open') this.$drop[this.animateMethod('hide')]() if (this.options.container) { this.$parent.append(this.$drop) this.$drop.css({ 'top': 'auto', 'left': 'auto' }) } this.options.onClose() } animateMethod (method) { const methods = { show: { fade: 'fadeIn', slide: 'slideDown' }, hide: { fade: 'fadeOut', slide: 'slideUp' } } return methods[method][this.options.animate] || method } update (ignoreTrigger) { const valueSelects = this.getSelects() let textSelects = this.getSelects('text') if (this.options.displayValues) { textSelects = valueSelects } const $span = this.$choice.find('>span') const sl = valueSelects.length let html = '' if (sl === 0) { $span.addClass('placeholder').html(this.options.placeholder) } else if (sl < this.options.minimumCountSelected) { html = textSelects.join(this.options.displayDelimiter) } else if (this.options.formatAllSelected() && sl === this.dataTotal) { html = this.options.formatAllSelected() } else if (this.options.ellipsis && sl > this.options.minimumCountSelected) { html = `${textSelects.slice(0, this.options.minimumCountSelected) .join(this.options.displayDelimiter)}...` } else if (this.options.formatCountSelected() && sl > this.options.minimumCountSelected) { html = this.options.formatCountSelected(sl, this.dataTotal) } else { html = textSelects.join(this.options.displayDelimiter) } if (html) { $span.removeClass('placeholder').html(html) } if (this.options.displayTitle) { $span.prop('title', this.getSelects('text')) } // set selects to select this.$el.val(this.getSelects()) // trigger