multiple-select
Version:
Multiple select is a jQuery plugin to select multiple elements with checkboxes :).
1,500 lines (1,482 loc) • 49.4 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) :
typeof define === 'function' && define.amd ? define(['jquery'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jQuery));
})(this, (function ($) { 'use strict';
const BLOCK_ROWS = 500;
const CLUSTER_BLOCKS = 4;
const DEFAULTS = {
name: '',
placeholder: '',
classes: '',
classPrefix: '',
classInput: '',
data: undefined,
locale: undefined,
selectAll: true,
single: undefined,
singleRadio: false,
multiple: false,
hideOptgroupCheckboxes: false,
multipleWidth: 80,
width: undefined,
size: undefined,
dropWidth: undefined,
maxHeight: 250,
maxHeightUnit: 'px',
position: 'bottom',
displayValues: false,
displayTitle: false,
displayDelimiter: ', ',
minimumCountSelected: 3,
ellipsis: false,
isOpen: false,
keepOpen: false,
openOnHover: false,
container: null,
filter: false,
filterGroup: false,
filterPlaceholder: '',
filterAcceptOnEnter: false,
filterByDataLength: undefined,
filterSelectAll: true,
customFilter({
text,
label,
search
}) {
return (label || text).includes(search);
},
showClear: false,
animate: undefined,
styler() {
return false;
},
textTemplate($elm) {
return $elm[0].innerHTML.trim();
},
labelTemplate($elm) {
return $elm[0].getAttribute('label');
},
onOpen() {
return false;
},
onClose() {
return false;
},
onCheckAll() {
return false;
},
onUncheckAll() {
return false;
},
onFocus() {
return false;
},
onBlur() {
return false;
},
onOptgroupClick() {
return false;
},
onBeforeClick() {
return true;
},
onClick() {
return false;
},
onFilter() {
return false;
},
onClear() {
return false;
},
onAfterCreate() {
return false;
}
};
const EN = {
formatSelectAll() {
return '[Select all]';
},
formatAllSelected() {
return 'All selected';
},
formatCountSelected(count, total) {
return `${count} of ${total} selected`;
},
formatNoMatchesFound() {
return 'No matches found';
}
};
const METHODS = ['getOptions', 'refreshOptions', 'getData', 'getSelects', 'setSelects', 'enable', 'disable', 'open', 'close', 'check', 'uncheck', 'checkAll', 'uncheckAll', 'checkInvert', 'focus', 'blur', 'refresh', 'resetFilter', 'destroy'];
Object.assign(DEFAULTS, EN);
const Constants = {
BLOCK_ROWS,
CLUSTER_BLOCKS,
DEFAULTS,
METHODS,
LOCALES: {
en: EN,
'en-US': EN
}
};
class VirtualScroll {
constructor(options) {
this.rows = options.rows;
this.scrollEl = options.scrollEl;
this.contentEl = options.contentEl;
this.callback = options.callback;
this.cache = {};
this.scrollTop = this.scrollEl.scrollTop;
this.initDOM(this.rows);
this.scrollEl.scrollTop = this.scrollTop;
this.lastCluster = 0;
const onScroll = () => {
if (this.lastCluster !== (this.lastCluster = this.getNum())) {
this.initDOM(this.rows);
this.callback();
}
};
this.scrollEl.addEventListener('scroll', onScroll, false);
this.destroy = () => {
this.contentEl.innerHtml = '';
this.scrollEl.removeEventListener('scroll', onScroll, false);
};
}
initDOM(rows) {
if (typeof this.clusterHeight === 'undefined') {
this.cache.scrollTop = this.scrollEl.scrollTop;
this.cache.data = this.contentEl.innerHTML = rows[0] + rows[0] + rows[0];
this.getRowsHeight(rows);
}
const data = this.initData(rows, this.getNum());
const thisRows = data.rows.join('');
const dataChanged = this.checkChanges('data', thisRows);
const topOffsetChanged = this.checkChanges('top', data.topOffset);
const bottomOffsetChanged = this.checkChanges('bottom', data.bottomOffset);
const html = [];
if (dataChanged && topOffsetChanged) {
if (data.topOffset) {
html.push(this.getExtra('top', data.topOffset));
}
html.push(thisRows);
if (data.bottomOffset) {
html.push(this.getExtra('bottom', data.bottomOffset));
}
this.contentEl.innerHTML = html.join('');
} else if (bottomOffsetChanged) {
this.contentEl.lastChild.style.height = `${data.bottomOffset}px`;
}
}
getRowsHeight() {
if (typeof this.itemHeight === 'undefined') {
const nodes = this.contentEl.children;
const node = nodes[Math.floor(nodes.length / 2)];
this.itemHeight = node.offsetHeight;
}
this.blockHeight = this.itemHeight * Constants.BLOCK_ROWS;
this.clusterRows = Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS;
this.clusterHeight = this.blockHeight * Constants.CLUSTER_BLOCKS;
}
getNum() {
this.scrollTop = this.scrollEl.scrollTop;
return Math.floor(this.scrollTop / (this.clusterHeight - this.blockHeight)) || 0;
}
initData(rows, num) {
if (rows.length < Constants.BLOCK_ROWS) {
return {
topOffset: 0,
bottomOffset: 0,
rowsAbove: 0,
rows
};
}
const start = Math.max((this.clusterRows - Constants.BLOCK_ROWS) * num, 0);
const end = start + this.clusterRows;
const topOffset = Math.max(start * this.itemHeight, 0);
const bottomOffset = Math.max((rows.length - end) * this.itemHeight, 0);
const thisRows = [];
let rowsAbove = start;
if (topOffset < 1) {
rowsAbove++;
}
for (let i = start; i < end; i++) {
rows[i] && thisRows.push(rows[i]);
}
this.dataStart = start;
this.dataEnd = end;
return {
topOffset,
bottomOffset,
rowsAbove,
rows: thisRows
};
}
checkChanges(type, value) {
const changed = value !== this.cache[type];
this.cache[type] = value;
return changed;
}
getExtra(className, height) {
const tag = document.createElement('li');
tag.className = `virtual-scroll-${className}`;
if (height) {
tag.style.height = `${height}px`;
}
return tag.outerHTML;
}
}
const compareObjects = (objectA, objectB, compareLength) => {
const aKeys = Object.keys(objectA);
const bKeys = Object.keys(objectB);
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if (bKeys.includes(key) && objectA[key] !== objectB[key]) {
return false;
}
}
return true;
};
const findByParam = (data, param, value) => {
for (const row of data) {
if (row[param] === value || row[param] === `${+row[param]}` && +row[param] === value) {
return row;
}
if (row.type === 'optgroup') {
for (const child of row.children) {
if (child[param] === value || child[param] === `${+child[param]}` && +child[param] === value) {
return child;
}
}
}
}
};
const getDocumentClickEvent = (id = '') => {
id = id || `${+new Date()}${~~(Math.random() * 1000000)}`;
return `click.multiple-select-${id}`;
};
const removeDiacritics = str => {
if (str.normalize) {
return str.normalize('NFD').replace(/[\u0300-\u036F]/g, '');
}
const defaultDiacriticsRemovalMap = [{
base: 'A',
letters: /[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g
}, {
base: 'AA',
letters: /[\uA732]/g
}, {
base: 'AE',
letters: /[\u00C6\u01FC\u01E2]/g
}, {
base: 'AO',
letters: /[\uA734]/g
}, {
base: 'AU',
letters: /[\uA736]/g
}, {
base: 'AV',
letters: /[\uA738\uA73A]/g
}, {
base: 'AY',
letters: /[\uA73C]/g
}, {
base: 'B',
letters: /[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g
}, {
base: 'C',
letters: /[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g
}, {
base: 'D',
letters: /[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g
}, {
base: 'DZ',
letters: /[\u01F1\u01C4]/g
}, {
base: 'Dz',
letters: /[\u01F2\u01C5]/g
}, {
base: 'E',
letters: /[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g
}, {
base: 'F',
letters: /[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g
}, {
base: 'G',
letters: /[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g
}, {
base: 'H',
letters: /[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g
}, {
base: 'I',
letters: /[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g
}, {
base: 'J',
letters: /[\u004A\u24BF\uFF2A\u0134\u0248]/g
}, {
base: 'K',
letters: /[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g
}, {
base: 'L',
letters: /[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g
}, {
base: 'LJ',
letters: /[\u01C7]/g
}, {
base: 'Lj',
letters: /[\u01C8]/g
}, {
base: 'M',
letters: /[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g
}, {
base: 'N',
letters: /[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g
}, {
base: 'NJ',
letters: /[\u01CA]/g
}, {
base: 'Nj',
letters: /[\u01CB]/g
}, {
base: 'O',
letters: /[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g
}, {
base: 'OI',
letters: /[\u01A2]/g
}, {
base: 'OO',
letters: /[\uA74E]/g
}, {
base: 'OU',
letters: /[\u0222]/g
}, {
base: 'P',
letters: /[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g
}, {
base: 'Q',
letters: /[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g
}, {
base: 'R',
letters: /[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g
}, {
base: 'S',
letters: /[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g
}, {
base: 'T',
letters: /[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g
}, {
base: 'TZ',
letters: /[\uA728]/g
}, {
base: 'U',
letters: /[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g
}, {
base: 'V',
letters: /[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g
}, {
base: 'VY',
letters: /[\uA760]/g
}, {
base: 'W',
letters: /[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g
}, {
base: 'X',
letters: /[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g
}, {
base: 'Y',
letters: /[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g
}, {
base: 'Z',
letters: /[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g
}, {
base: 'a',
letters: /[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g
}, {
base: 'aa',
letters: /[\uA733]/g
}, {
base: 'ae',
letters: /[\u00E6\u01FD\u01E3]/g
}, {
base: 'ao',
letters: /[\uA735]/g
}, {
base: 'au',
letters: /[\uA737]/g
}, {
base: 'av',
letters: /[\uA739\uA73B]/g
}, {
base: 'ay',
letters: /[\uA73D]/g
}, {
base: 'b',
letters: /[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g
}, {
base: 'c',
letters: /[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g
}, {
base: 'd',
letters: /[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g
}, {
base: 'dz',
letters: /[\u01F3\u01C6]/g
}, {
base: 'e',
letters: /[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g
}, {
base: 'f',
letters: /[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g
}, {
base: 'g',
letters: /[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g
}, {
base: 'h',
letters: /[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g
}, {
base: 'hv',
letters: /[\u0195]/g
}, {
base: 'i',
letters: /[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g
}, {
base: 'j',
letters: /[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g
}, {
base: 'k',
letters: /[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g
}, {
base: 'l',
letters: /[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g
}, {
base: 'lj',
letters: /[\u01C9]/g
}, {
base: 'm',
letters: /[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g
}, {
base: 'n',
letters: /[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g
}, {
base: 'nj',
letters: /[\u01CC]/g
}, {
base: 'o',
letters: /[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g
}, {
base: 'oi',
letters: /[\u01A3]/g
}, {
base: 'ou',
letters: /[\u0223]/g
}, {
base: 'oo',
letters: /[\uA74F]/g
}, {
base: 'p',
letters: /[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g
}, {
base: 'q',
letters: /[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g
}, {
base: 'r',
letters: /[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g
}, {
base: 's',
letters: /[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g
}, {
base: 't',
letters: /[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g
}, {
base: 'tz',
letters: /[\uA729]/g
}, {
base: 'u',
letters: /[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g
}, {
base: 'v',
letters: /[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g
}, {
base: 'vy',
letters: /[\uA761]/g
}, {
base: 'w',
letters: /[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g
}, {
base: 'x',
letters: /[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g
}, {
base: 'y',
letters: /[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g
}, {
base: 'z',
letters: /[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g
}];
return defaultDiacriticsRemovalMap.reduce((string, {
letters,
base
}) => string.replace(letters, base), str);
};
const removeUndefined = obj => {
Object.keys(obj).forEach(key => obj[key] === undefined ? delete obj[key] : '');
return obj;
};
const setDataKeys = data => {
let total = 0;
data.forEach((row, i) => {
if (row.type === 'optgroup') {
row._key = `group_${i}`;
row.visible = typeof row.visible === 'undefined' ? true : row.visible;
row.children.forEach((child, j) => {
child.visible = typeof child.visible === 'undefined' ? true : child.visible;
if (!child.divider) {
child._key = `option_${i}_${j}`;
total += 1;
}
});
} else {
row.visible = typeof row.visible === 'undefined' ? true : row.visible;
if (!row.divider) {
row._key = `option_${i}`;
total += 1;
}
}
});
return total;
};
const toRaw = proxy => {
if (proxy && typeof proxy === 'object' && proxy.__v_raw) {
return proxy.__v_raw;
}
return proxy;
};
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 || '';
if (this.options.classes) {
this.$el.addClass(this.options.classes);
}
if (this.options.classPrefix) {
this.$el.addClass(this.options.classPrefix);
if (this.options.size) {
this.$el.addClass(`${this.options.classPrefix}-${this.options.size}`);
}
}
// 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 = $(`
<div class="ms-parent ${el.getAttribute('class') || ''} ${this.options.classes}"
title="${el.getAttribute('title') || ''}" />
`);
// 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) {
tabIndex = this.tabIndex && `tabindex="${this.tabIndex}"`;
}
this.$el.attr('tabindex', -1);
this.$choice = $(`
<button type="button" class="ms-choice"${tabIndex}>
<span class="ms-placeholder">${this.options.placeholder}</span>
${this.options.showClear ? '<div class="icon-close"></div>' : ''}
<div class="icon-caret"></div>
</button>
`);
// default position is bottom
this.$drop = $(`<div class="ms-drop ${this.options.position}" />`);
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
};
}
if (it.children?.length) {
return {
...it,
children: it.children.map(it => ({
...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._value || $elm.data('value')) {
row._value = elm._value || $elm.data('value'); // value for object
}
if (Object.keys($elm.data()).length) {
row._data = $elm.data();
if (row._data.divider) {
row.divider = row._data.divider;
}
}
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 => child.selected && !child.disabled && child.visible).length;
if (row.children.length) {
row.selected = !this.options.single && selectedCount && selectedCount === row.children.filter(child => !child.disabled && child.visible && !child.divider).length;
}
selectedTotal += selectedCount;
} else {
selectedTotal += row.selected && !row.disabled && row.visible ? 1 : 0;
}
}
this.allSelected = this.data.filter(row => row.selected && !row.disabled && row.visible).length === this.data.filter(row => !row.disabled && row.visible && !row.divider).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(`
<div class="ms-search">
<input type="text" autocomplete="off" autocorrect="off"
autocapitalize="off" spellcheck="false"
placeholder="${this.options.filterPlaceholder}">
</div>
`);
}
html.push('<ul></ul>');
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', -1e4).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 && (this.options.filterSelectAll || !this.filterText)) {
rows.push(`
<li class="ms-select-all" tabindex="0">
<label>
<input
type="checkbox" ${this.selectAllName}
${this.allSelected ? ' checked="checked"' : ''}
${this.options.classInput ? `class="${this.options.classInput}"` : ''}
tabindex="-1"
/>
<span>${this.options.formatSelectAll()}</span>
</label>
</li>
`);
}
this.updateData = [];
this.data.forEach(row => {
rows.push(...this.initListItem(row));
});
rows.push(`<li class="ms-no-results">${this.options.formatNoMatchesFound()}</li>`);
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 ? `<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"' : ''}
${this.options.classInput ? `class="${this.options.classInput}"` : ''}
tabindex="-1"
>`;
if (!classes.includes('hide-radio') && (this.options.hideOptgroupCheckboxes || this.options.single)) {
classes += 'hide-radio ';
}
html.push(`
<li class="group ${classes}" ${style} tabindex="${classes.includes('hide-radio') || row.disabled ? -1 : 0}">
<label class="optgroup${this.options.single || row.disabled ? ' disabled' : ''}">
${group}${row.label}
</label>
</li>
`);
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 || ''} option-level-${level} `;
if (row.divider) {
return '<li class="option-divider"/>';
}
return [`
<li class="${multiple} ${classes}" ${title} ${style} tabindex="${row.disabled ? -1 : 0}">
<label class="${row.disabled ? 'disabled' : ''}">
<input type="${type}"
value="${row.value}"
data-key="${row._key}"
${this.selectItemName}
${row.selected ? ' checked="checked"' : ''}
${row.disabled ? ' disabled="disabled"' : ''}
${this.options.classInput ? `class="${this.options.classInput}"` : ''}
tabindex="-1"
>
<span>${row.text}</span>
</label>
</li>
`];
}
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 => 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'));
const close = () => {
if (this.options.single && this.options.isOpen && !this.options.keepOpen) {
this.close();
}
};
if (this.options.onBeforeClick(option) === false) {
close();
return;
}
this._check(option, checked);
this.options.onClick(removeUndefined({
text: option.text,
value: option.value,
selected: option.selected,
data: option._data
}));
close();
});
this.$ul.find('li').off('keydown').on('keydown', e => {
const $this = $(e.currentTarget);
let $divider;
let $li;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
$divider = $this.prev('li.option-divider');
$li = $divider.length ? $divider : $this;
$li.prev().trigger('focus');
break;
case 'ArrowDown':
e.preventDefault();
$divider = $this.next('li.option-divider');
$li = $divider.length ? $divider : $this;
$li.next().trigger('focus');
break;
case 'Enter':
e.preventDefault();
$this.find('input').trigger('click');
if (this.options.single) {
this.$choice.trigger('focus');
}
break;
// ignore
}
});
}
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.$parent.addClass('ms-parent-open');
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.$parent.removeClass('ms-parent-open');
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('ms-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('ms-placeholder').html(html);
}
if (this.options.displayTitle) {
$span.prop('title', this.getSelects('text'));
}
// set selects to select
this.$el.val(this.getSelects());
// trigger <select> change event
if (!ignoreTrigger) {
this.$el.trigger('change');
}
}
updateSelected() {
for (let i = this.updateDataStart; i < this.updateDataEnd; i++) {
const row = this.updateData[i];
this.$drop.find(`input[data-key=${row._key}]`).prop('checked', row.selected).closest('li').toggleClass('selected', row.selected);
}
const noResult = this.data.filter(row => row.visible).length === 0;
if (this.$selectAll.length) {
this.$selectAll.prop('checked', this.allSelected).closest('li').toggleClass('selected', this.allSelected).toggle(!noResult);
}
this.$noResults.toggle(noResult);
if (this.virtualScroll) {
this.virtualScroll.rows = this.getListRows();
}
}
getData() {
return this.options.data;
}
getOptions() {
// deep copy and remove data
const options = $.extend({}, this.options);
delete options.data;
return $.extend(true, {}, options);
}
refreshOptions(options) {
// If the objects are equivalent then avoid the call of destroy / init methods
if (compareObjects(this.options, options)) {
return;
}
this.options = $.extend(this.options, options);
this.destroy();
this.init();
}
// value html, or text, default: 'value'
getSelects(type = 'value') {
const values = [];
for (const row of this.data) {
if (row.type === 'optgroup') {
const selectedChildren = row.children.filter(child => child.selected);
if (!selectedChildren.length) {
continue;
}
if (type === 'value' || this.options.single) {
values.push(...selectedChildren.map(child => type === 'value' ? child._value || child[type] : child[type]));
} else {
const value = [];
value.push('[');
value.push(row.label);
value.push(`: ${selectedChildren.map(child => child[type]).join(', ')}`);
value.push(']');
values.push(value.join(''));
}
} else if (row.selected) {
values.push(type === 'value' ? row._value || row[type] : row[type]);
}
}
return values;
}
setSelects(values, type = 'value', ignoreTrigger = false) {
let hasChanged = false;
const _setSelects = rows => {
for (const row of rows) {
let selected = false;
if (type === 'text') {
selected = values.includes($('<div>').html(row.text).text().trim());
} else {
const value = toRaw(row._value || row.value);
selected = values.some(item => toRaw(item) === value);
if (!selected && row.value === `${+row.value}`) {
selected = values.includes(+row.value);
}
}
if (row.selected !== selected) {
hasChanged = true;
}
row.selected = selected;
}
};
for (const row of this.data) {
if (row.type === 'optgroup') {
_setSelects(row.children);
} else {
_setSelects([row]);
}
}
if (hasChanged) {
this.initSelected(ignoreTrigger);
this.updateSelected();
this.update(ignoreTrigger);
}
}
enable() {
this.$choice.removeClass('disabled');
}
disable() {
this.$choice.addClass('disabled');
}
check(value) {
const option = findByParam(this.data, 'value', value);
if (!option) {
return;
}
this._check(option, true);
}
uncheck(value) {
const option = findByParam(this.data, 'value', value);
if (!option) {
return;
}
this._check(option, false);
}
_check(option, checked) {
if (this.options.single) {
this._checkAll(false, true);
}
option.selected = checked;
this.initSelected();
this.updateSelected();
this.update();
}
checkAll() {
this._checkAll(true);
}
uncheckAll() {
this._checkAll(false);
}
_checkAll(checked, ignoreUpdate) {
for (const row of this.data) {
if (row.type === 'optgroup') {
this._checkGroup(row, checked, true);
} else if (!row.disabled && !row.divider && (ignoreUpdate || row.visible)) {
row.selected = checked;
}
}
if (!ignoreUpdate) {
this.initSelected();
this.updateSelected();
this.update();
}
}
_checkGroup(group, checked, ignoreUpdate) {
group.selected = checked;
group.children.forEach(row => {
if (!row.disabled && !row.divider && (ignoreUpdate || row.visible)) {
row.selected = checked;
}
});
if (!ignoreUpdate) {
this.initSelected();
this.updateSelected();
this.update();
}
}
checkInvert() {
if (this.options.single) {
return;
}
for (const row of this.data) {
if (row.type === 'optgroup') {
for (const child of row.children) {
if (!child.divider) {
child.selected = !child.selected;
}
}
} else if (!row.divider) {
row.selected = !row.selected;
}
}
this.initSelected();
this.updateSelected();
this.update();
}
focus() {
this.$choice.focus();
this.options.onFocus();
}
blur() {
this.$choice.blur();
this.options.onBlur();
}
refresh() {
this.destroy();
this.init();
}
resetFilter() {
if (this.options.filter) {
this.$searchInput.val('');
this.filter(true);
}
}
filter(ignoreTrigger) {
const originalSearch = this.$searchInput.val().trim();
const search = originalSearch.toLowerCase();
if (this.filterText === search) {
return;
}
this.filterText = search;
for (const row of this.data) {
if (row.type === 'optgroup') {
if (this.options.filterGroup) {
const visible = this.options.customFilter({
label: removeDiacritics(row.label.toString().toLowerCase()),
search: removeDiacritics(search),
originalLabel: row.label,
originalSearch,
row
});
row.visible = visible;
for (const child of row.children) {
child.visible = visible;
}
} else {
for (const child of row.children) {
child.visible = this.options.customFilter({
text: removeDiacritics(child.text.toString().toLowerCase()),
search: removeDiacritics(search),
originalText: child.text,
originalSearch,
row: child,
parent: row
});
}
row.visible = row.children.filter(child => child.visible).length > 0;
}
} else {
row.visible = this.options.customFilter({
text: removeDiacritics(row.text.toString().toLowerCase()),
search: removeDiacritics(search),
originalText: row.text,
originalSearch,
row
});
}
}
this.initListItems();
this.initSelected(ignoreTrigger);
this.updateSelected();
if (!ignoreTrigger) {
this.options.onFilter(originalSearch);
}
}
destroy() {
if (!this.$parent) {
return;
}
this.$el.before(this.$parent).removeClass('ms-offscreen');
if (this.tabIndex !== null) {
this.$el.attr('tabindex', this.tabIndex);
}
this.$parent.remove();
if (this.fromHtml) {
delete this.options.data;
this.fromHtml = false;
}
}
}
$.fn.multipleSelect = function (option, ...args) {
let value;
this.each((i, el) => {
const $this = $(el);
let data = $this.data('multipleSelect');
const options = $.extend({}, $this.data(), typeof option === 'object' && option);
if (!data) {
data = new MultipleSelect($this, options);
$this.data('multipleSelect', data);
}
if (typeof option === 'string') {
if ($.inArray(option, Constants.METHODS) < 0) {
throw new Error(`Unknown method: ${option}`);
}
value = data[option](...args);
if (option === 'destroy') {
$this.removeData('multipleSelect');
}
} else {
data.init();
}
});
return typeof value !== 'undefined' ? value : this;
};
$.fn.multipleSelect.Constructor = MultipleSelect;
$.fn.multipleSelect.defaults = Constants.DEFAULTS;
$.fn.multipleSelect.locales = Constants.LOCALES;
$.fn.multipleSelect.methods = Constants.METHODS;
}));