UNPKG

doc-fui-ds

Version:

Doc

1,919 lines (1,645 loc) 102 kB
/** * Tom Select v2.0.3 * Licensed under the Apache License, Version 2.0 (the "License"); */ 'use strict'; /** * MicroEvent - to make any js object an event emitter * * - pure javascript - server compatible, browser compatible * - dont rely on the browser doms * - super simple - you get it immediatly, no mistery, no magic involved * * @author Jerome Etienne (https://github.com/jeromeetienne) */ /** * Execute callback for each event in space separated list of event names * */ function forEvents(events, callback) { events.split(/\s+/).forEach(event => { callback(event); }); } class MicroEvent { constructor() { this._events = void 0; this._events = {}; } on(events, fct) { forEvents(events, event => { this._events[event] = this._events[event] || []; this._events[event].push(fct); }); } off(events, fct) { var n = arguments.length; if (n === 0) { this._events = {}; return; } forEvents(events, event => { if (n === 1) return delete this._events[event]; if (event in this._events === false) return; this._events[event].splice(this._events[event].indexOf(fct), 1); }); } trigger(events, ...args) { var self = this; forEvents(events, event => { if (event in self._events === false) return; for (let fct of self._events[event]) { fct.apply(self, args); } }); } } /** * microplugin.js * Copyright (c) 2013 Brian Reavis & contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this * file except in compliance with the License. You may obtain a copy of the License at: * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. * * @author Brian Reavis <brian@thirdroute.com> */ function MicroPlugin(Interface) { Interface.plugins = {}; return class extends Interface { constructor(...args) { super(...args); this.plugins = { names: [], settings: {}, requested: {}, loaded: {} }; } /** * Registers a plugin. * * @param {function} fn */ static define(name, fn) { Interface.plugins[name] = { 'name': name, 'fn': fn }; } /** * Initializes the listed plugins (with options). * Acceptable formats: * * List (without options): * ['a', 'b', 'c'] * * List (with options): * [{'name': 'a', options: {}}, {'name': 'b', options: {}}] * * Hash (with options): * {'a': { ... }, 'b': { ... }, 'c': { ... }} * * @param {array|object} plugins */ initializePlugins(plugins) { var key, name; const self = this; const queue = []; if (Array.isArray(plugins)) { plugins.forEach(plugin => { if (typeof plugin === 'string') { queue.push(plugin); } else { self.plugins.settings[plugin.name] = plugin.options; queue.push(plugin.name); } }); } else if (plugins) { for (key in plugins) { if (plugins.hasOwnProperty(key)) { self.plugins.settings[key] = plugins[key]; queue.push(key); } } } while (name = queue.shift()) { self.require(name); } } loadPlugin(name) { var self = this; var plugins = self.plugins; var plugin = Interface.plugins[name]; if (!Interface.plugins.hasOwnProperty(name)) { throw new Error('Unable to find "' + name + '" plugin'); } plugins.requested[name] = true; plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]); plugins.names.push(name); } /** * Initializes a plugin. * */ require(name) { var self = this; var plugins = self.plugins; if (!self.plugins.loaded.hasOwnProperty(name)) { if (plugins.requested[name]) { throw new Error('Plugin has circular dependency ("' + name + '")'); } self.loadPlugin(name); } return plugins.loaded[name]; } }; } // https://github.com/andrewrk/node-diacritics/blob/master/index.js var latin_pat; const accent_pat = '[\u0300-\u036F\u{b7}\u{2be}]'; // \u{2bc} const accent_reg = new RegExp(accent_pat, 'g'); var diacritic_patterns; const latin_convert = { 'æ': 'ae', 'ⱥ': 'a', 'ø': 'o' }; const convert_pat = new RegExp(Object.keys(latin_convert).join('|'), 'g'); /** * code points generated from toCodePoints(); * removed 65339 to 65345 */ const code_points = [[67, 67], [160, 160], [192, 438], [452, 652], [961, 961], [1019, 1019], [1083, 1083], [1281, 1289], [1984, 1984], [5095, 5095], [7429, 7441], [7545, 7549], [7680, 7935], [8580, 8580], [9398, 9449], [11360, 11391], [42792, 42793], [42802, 42851], [42873, 42897], [42912, 42922], [64256, 64260], [65313, 65338], [65345, 65370]]; /** * Remove accents * via https://github.com/krisk/Fuse/issues/133#issuecomment-318692703 * */ const asciifold = str => { return str.normalize('NFKD').replace(accent_reg, '').toLowerCase().replace(convert_pat, function (foreignletter) { return latin_convert[foreignletter]; }); }; /** * Convert array of strings to a regular expression * ex ['ab','a'] => (?:ab|a) * ex ['a','b'] => [ab] * */ const arrayToPattern = (chars, glue = '|') => { if (chars.length == 1) { return chars[0]; } var longest = 1; chars.forEach(a => { longest = Math.max(longest, a.length); }); if (longest == 1) { return '[' + chars.join('') + ']'; } return '(?:' + chars.join(glue) + ')'; }; /** * Get all possible combinations of substrings that add up to the given string * https://stackoverflow.com/questions/30169587/find-all-the-combination-of-substrings-that-add-up-to-the-given-string * */ const allSubstrings = input => { if (input.length === 1) return [[input]]; var result = []; allSubstrings(input.substring(1)).forEach(function (subresult) { var tmp = subresult.slice(0); tmp[0] = input.charAt(0) + tmp[0]; result.push(tmp); tmp = subresult.slice(0); tmp.unshift(input.charAt(0)); result.push(tmp); }); return result; }; /** * Generate a list of diacritics from the list of code points * */ const generateDiacritics = () => { var diacritics = {}; code_points.forEach(code_range => { for (let i = code_range[0]; i <= code_range[1]; i++) { let diacritic = String.fromCharCode(i); let latin = asciifold(diacritic); if (latin == diacritic.toLowerCase()) { continue; } if (!(latin in diacritics)) { diacritics[latin] = [latin]; } var patt = new RegExp(arrayToPattern(diacritics[latin]), 'iu'); if (diacritic.match(patt)) { continue; } diacritics[latin].push(diacritic); } }); var latin_chars = Object.keys(diacritics); // latin character pattern // match longer substrings first latin_chars = latin_chars.sort((a, b) => b.length - a.length); latin_pat = new RegExp('(' + arrayToPattern(latin_chars) + accent_pat + '*)', 'g'); // build diacritic patterns // ae needs: // (?:(?:ae|Æ|Ǽ|Ǣ)|(?:A|Ⓐ|A...)(?:E|ɛ|Ⓔ...)) var diacritic_patterns = {}; latin_chars.sort((a, b) => a.length - b.length).forEach(latin => { var substrings = allSubstrings(latin); var pattern = substrings.map(sub_pat => { sub_pat = sub_pat.map(l => { if (diacritics.hasOwnProperty(l)) { return arrayToPattern(diacritics[l]); } return l; }); return arrayToPattern(sub_pat, ''); }); diacritic_patterns[latin] = arrayToPattern(pattern); }); return diacritic_patterns; }; /** * Expand a regular expression pattern to include diacritics * eg /a/ becomes /aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐɑAⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ/ * */ const diacriticRegexPoints = regex => { if (diacritic_patterns === undefined) { diacritic_patterns = generateDiacritics(); } const decomposed = regex.normalize('NFKD').toLowerCase(); return decomposed.split(latin_pat).map(part => { if (part == '') { return ''; } // "ffl" or "ffl" const no_accent = asciifold(part); if (diacritic_patterns.hasOwnProperty(no_accent)) { return diacritic_patterns[no_accent]; } // 'أهلا' (\u{623}\u{647}\u{644}\u{627}) or 'أهلا' (\u{627}\u{654}\u{647}\u{644}\u{627}) const composed_part = part.normalize('NFC'); if (composed_part != part) { return arrayToPattern([part, composed_part]); } return part; }).join(''); }; // @ts-ignore TS2691 "An import path cannot end with a '.ts' extension" /** * A property getter resolving dot-notation * @param {Object} obj The root object to fetch property on * @param {String} name The optionally dotted property name to fetch * @return {Object} The resolved property value */ const getAttr = (obj, name) => { if (!obj) return; return obj[name]; }; /** * A property getter resolving dot-notation * @param {Object} obj The root object to fetch property on * @param {String} name The optionally dotted property name to fetch * @return {Object} The resolved property value */ const getAttrNesting = (obj, name) => { if (!obj) return; var part, names = name.split("."); while ((part = names.shift()) && (obj = obj[part])); return obj; }; /** * Calculates how close of a match the * given value is against a search token. * */ const scoreValue = (value, token, weight) => { var score, pos; if (!value) return 0; value = value + ''; pos = value.search(token.regex); if (pos === -1) return 0; score = token.string.length / value.length; if (pos === 0) score += 0.5; return score * weight; }; /** * * https://stackoverflow.com/questions/63006601/why-does-u-throw-an-invalid-escape-error */ const escape_regex = str => { return (str + '').replace(/([\$\(-\+\.\?\[-\^\{-\}])/g, '\\$1'); }; /** * Cast object property to an array if it exists and has a value * */ const propToArray = (obj, key) => { var value = obj[key]; if (typeof value == 'function') return value; if (value && !Array.isArray(value)) { obj[key] = [value]; } }; /** * Iterates over arrays and hashes. * * ``` * iterate(this.items, function(item, id) { * // invoked for each item * }); * ``` * */ const iterate = (object, callback) => { if (Array.isArray(object)) { object.forEach(callback); } else { for (var key in object) { if (object.hasOwnProperty(key)) { callback(object[key], key); } } } }; const cmp = (a, b) => { if (typeof a === 'number' && typeof b === 'number') { return a > b ? 1 : a < b ? -1 : 0; } a = asciifold(a + '').toLowerCase(); b = asciifold(b + '').toLowerCase(); if (a > b) return 1; if (b > a) return -1; return 0; }; /** * sifter.js * Copyright (c) 2013–2020 Brian Reavis & contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this * file except in compliance with the License. You may obtain a copy of the License at: * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. * * @author Brian Reavis <brian@thirdroute.com> */ class Sifter { // []|{}; /** * Textually searches arrays and hashes of objects * by property (or multiple properties). Designed * specifically for autocomplete. * */ constructor(items, settings) { this.items = void 0; this.settings = void 0; this.items = items; this.settings = settings || { diacritics: true }; } /** * Splits a search string into an array of individual * regexps to be used to match results. * */ tokenize(query, respect_word_boundaries, weights) { if (!query || !query.length) return []; const tokens = []; const words = query.split(/\s+/); var field_regex; if (weights) { field_regex = new RegExp('^(' + Object.keys(weights).map(escape_regex).join('|') + ')\:(.*)$'); } words.forEach(word => { let field_match; let field = null; let regex = null; // look for "field:query" tokens if (field_regex && (field_match = word.match(field_regex))) { field = field_match[1]; word = field_match[2]; } if (word.length > 0) { regex = escape_regex(word); if (this.settings.diacritics) { regex = diacriticRegexPoints(regex); } if (respect_word_boundaries) regex = "\\b" + regex; } tokens.push({ string: word, regex: regex ? new RegExp(regex, 'iu') : null, field: field }); }); return tokens; } /** * Returns a function to be used to score individual results. * * Good matches will have a higher score than poor matches. * If an item is not a match, 0 will be returned by the function. * * @returns {function} */ getScoreFunction(query, options) { var search = this.prepareSearch(query, options); return this._getScoreFunction(search); } _getScoreFunction(search) { const tokens = search.tokens, token_count = tokens.length; if (!token_count) { return function () { return 0; }; } const fields = search.options.fields, weights = search.weights, field_count = fields.length, getAttrFn = search.getAttrFn; if (!field_count) { return function () { return 1; }; } /** * Calculates the score of an object * against the search query. * */ const scoreObject = function () { if (field_count === 1) { return function (token, data) { const field = fields[0].field; return scoreValue(getAttrFn(data, field), token, weights[field]); }; } return function (token, data) { var sum = 0; // is the token specific to a field? if (token.field) { const value = getAttrFn(data, token.field); if (!token.regex && value) { sum += 1 / field_count; } else { sum += scoreValue(value, token, 1); } } else { iterate(weights, (weight, field) => { sum += scoreValue(getAttrFn(data, field), token, weight); }); } return sum / field_count; }; }(); if (token_count === 1) { return function (data) { return scoreObject(tokens[0], data); }; } if (search.options.conjunction === 'and') { return function (data) { var i = 0, score, sum = 0; for (; i < token_count; i++) { score = scoreObject(tokens[i], data); if (score <= 0) return 0; sum += score; } return sum / token_count; }; } else { return function (data) { var sum = 0; iterate(tokens, token => { sum += scoreObject(token, data); }); return sum / token_count; }; } } /** * Returns a function that can be used to compare two * results, for sorting purposes. If no sorting should * be performed, `null` will be returned. * * @return function(a,b) */ getSortFunction(query, options) { var search = this.prepareSearch(query, options); return this._getSortFunction(search); } _getSortFunction(search) { var i, n, implicit_score; const self = this, options = search.options, sort = !search.query && options.sort_empty ? options.sort_empty : options.sort, sort_flds = [], multipliers = []; if (typeof sort == 'function') { return sort.bind(this); } /** * Fetches the specified sort field value * from a search result item. * */ const get_field = function get_field(name, result) { if (name === '$score') return result.score; return search.getAttrFn(self.items[result.id], name); }; // parse options if (sort) { for (i = 0, n = sort.length; i < n; i++) { if (search.query || sort[i].field !== '$score') { sort_flds.push(sort[i]); } } } // the "$score" field is implied to be the primary // sort field, unless it's manually specified if (search.query) { implicit_score = true; for (i = 0, n = sort_flds.length; i < n; i++) { if (sort_flds[i].field === '$score') { implicit_score = false; break; } } if (implicit_score) { sort_flds.unshift({ field: '$score', direction: 'desc' }); } } else { for (i = 0, n = sort_flds.length; i < n; i++) { if (sort_flds[i].field === '$score') { sort_flds.splice(i, 1); break; } } } for (i = 0, n = sort_flds.length; i < n; i++) { multipliers.push(sort_flds[i].direction === 'desc' ? -1 : 1); } // build function const sort_flds_count = sort_flds.length; if (!sort_flds_count) { return null; } else if (sort_flds_count === 1) { const sort_fld = sort_flds[0].field; const multiplier = multipliers[0]; return function (a, b) { return multiplier * cmp(get_field(sort_fld, a), get_field(sort_fld, b)); }; } else { return function (a, b) { var i, result, field; for (i = 0; i < sort_flds_count; i++) { field = sort_flds[i].field; result = multipliers[i] * cmp(get_field(field, a), get_field(field, b)); if (result) return result; } return 0; }; } } /** * Parses a search query and returns an object * with tokens and fields ready to be populated * with results. * */ prepareSearch(query, optsUser) { const weights = {}; var options = Object.assign({}, optsUser); propToArray(options, 'sort'); propToArray(options, 'sort_empty'); // convert fields to new format if (options.fields) { propToArray(options, 'fields'); const fields = []; options.fields.forEach(field => { if (typeof field == 'string') { field = { field: field, weight: 1 }; } fields.push(field); weights[field.field] = 'weight' in field ? field.weight : 1; }); options.fields = fields; } return { options: options, query: query.toLowerCase().trim(), tokens: this.tokenize(query, options.respect_word_boundaries, weights), total: 0, items: [], weights: weights, getAttrFn: options.nesting ? getAttrNesting : getAttr }; } /** * Searches through all items and returns a sorted array of matches. * */ search(query, options) { var self = this, score, search; search = this.prepareSearch(query, options); options = search.options; query = search.query; // generate result scoring function const fn_score = options.score || self._getScoreFunction(search); // perform search and sort if (query.length) { iterate(self.items, (item, id) => { score = fn_score(item); if (options.filter === false || score > 0) { search.items.push({ 'score': score, 'id': id }); } }); } else { iterate(self.items, (_, id) => { search.items.push({ 'score': 1, 'id': id }); }); } const fn_sort = self._getSortFunction(search); if (fn_sort) search.items.sort(fn_sort); // apply limits search.total = search.items.length; if (typeof options.limit === 'number') { search.items = search.items.slice(0, options.limit); } return search; } } /** * Return a dom element from either a dom query string, jQuery object, a dom element or html string * https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 * * param query should be {} */ const getDom = query => { if (query.jquery) { return query[0]; } if (query instanceof HTMLElement) { return query; } if (isHtmlString(query)) { let div = document.createElement('div'); div.innerHTML = query.trim(); // Never return a text node of whitespace as the result return div.firstChild; } return document.querySelector(query); }; const isHtmlString = arg => { if (typeof arg === 'string' && arg.indexOf('<') > -1) { return true; } return false; }; const escapeQuery = query => { return query.replace(/['"\\]/g, '\\$&'); }; /** * Dispatch an event * */ const triggerEvent = (dom_el, event_name) => { var event = document.createEvent('HTMLEvents'); event.initEvent(event_name, true, false); dom_el.dispatchEvent(event); }; /** * Apply CSS rules to a dom element * */ const applyCSS = (dom_el, css) => { Object.assign(dom_el.style, css); }; /** * Add css classes * */ const addClasses = (elmts, ...classes) => { var norm_classes = classesArray(classes); elmts = castAsArray(elmts); elmts.map(el => { norm_classes.map(cls => { el.classList.add(cls); }); }); }; /** * Remove css classes * */ const removeClasses = (elmts, ...classes) => { var norm_classes = classesArray(classes); elmts = castAsArray(elmts); elmts.map(el => { norm_classes.map(cls => { el.classList.remove(cls); }); }); }; /** * Return arguments * */ const classesArray = args => { var classes = []; iterate(args, _classes => { if (typeof _classes === 'string') { _classes = _classes.trim().split(/[\11\12\14\15\40]/); } if (Array.isArray(_classes)) { classes = classes.concat(_classes); } }); return classes.filter(Boolean); }; /** * Create an array from arg if it's not already an array * */ const castAsArray = arg => { if (!Array.isArray(arg)) { arg = [arg]; } return arg; }; /** * Get the closest node to the evt.target matching the selector * Stops at wrapper * */ const parentMatch = (target, selector, wrapper) => { if (wrapper && !wrapper.contains(target)) { return; } while (target && target.matches) { if (target.matches(selector)) { return target; } target = target.parentNode; } }; /** * Get the first or last item from an array * * > 0 - right (last) * <= 0 - left (first) * */ const getTail = (list, direction = 0) => { if (direction > 0) { return list[list.length - 1]; } return list[0]; }; /** * Return true if an object is empty * */ const isEmptyObject = obj => { return Object.keys(obj).length === 0; }; /** * Get the index of an element amongst sibling nodes of the same type * */ const nodeIndex = (el, amongst) => { if (!el) return -1; amongst = amongst || el.nodeName; var i = 0; while (el = el.previousElementSibling) { if (el.matches(amongst)) { i++; } } return i; }; /** * Set attributes of an element * */ const setAttr = (el, attrs) => { iterate(attrs, (val, attr) => { if (val == null) { el.removeAttribute(attr); } else { el.setAttribute(attr, '' + val); } }); }; /** * Replace a node */ const replaceNode = (existing, replacement) => { if (existing.parentNode) existing.parentNode.replaceChild(replacement, existing); }; /** * highlight v3 | MIT license | Johann Burkard <jb@eaio.com> * Highlights arbitrary terms in a node. * * - Modified by Marshal <beatgates@gmail.com> 2011-6-24 (added regex) * - Modified by Brian Reavis <brian@thirdroute.com> 2012-8-27 (cleanup) */ const highlight = (element, regex) => { if (regex === null) return; // convet string to regex if (typeof regex === 'string') { if (!regex.length) return; regex = new RegExp(regex, 'i'); } // Wrap matching part of text node with highlighting <span>, e.g. // Soccer -> <span class="highlight">Soc</span>cer for regex = /soc/i const highlightText = node => { var match = node.data.match(regex); if (match && node.data.length > 0) { var spannode = document.createElement('span'); spannode.className = 'highlight'; var middlebit = node.splitText(match.index); middlebit.splitText(match[0].length); var middleclone = middlebit.cloneNode(true); spannode.appendChild(middleclone); replaceNode(middlebit, spannode); return 1; } return 0; }; // Recurse element node, looking for child text nodes to highlight, unless element // is childless, <script>, <style>, or already highlighted: <span class="hightlight"> const highlightChildren = node => { if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName) && (node.className !== 'highlight' || node.tagName !== 'SPAN')) { for (var i = 0; i < node.childNodes.length; ++i) { i += highlightRecursive(node.childNodes[i]); } } }; const highlightRecursive = node => { if (node.nodeType === 3) { return highlightText(node); } highlightChildren(node); return 0; }; highlightRecursive(element); }; /** * removeHighlight fn copied from highlight v5 and * edited to remove with(), pass js strict mode, and use without jquery */ const removeHighlight = el => { var elements = el.querySelectorAll("span.highlight"); Array.prototype.forEach.call(elements, function (el) { var parent = el.parentNode; parent.replaceChild(el.firstChild, el); parent.normalize(); }); }; const KEY_A = 65; const KEY_RETURN = 13; const KEY_ESC = 27; const KEY_LEFT = 37; const KEY_UP = 38; const KEY_RIGHT = 39; const KEY_DOWN = 40; const KEY_BACKSPACE = 8; const KEY_DELETE = 46; const KEY_TAB = 9; const IS_MAC = typeof navigator === 'undefined' ? false : /Mac/.test(navigator.userAgent); const KEY_SHORTCUT = IS_MAC ? 'metaKey' : 'ctrlKey'; // ctrl key or apple key for ma var defaults = { options: [], optgroups: [], plugins: [], delimiter: ',', splitOn: null, // regexp or string for splitting up values from a paste command persist: true, diacritics: true, create: null, createOnBlur: false, createFilter: null, highlight: true, openOnFocus: true, shouldOpen: null, maxOptions: 50, maxItems: null, hideSelected: null, duplicates: false, addPrecedence: false, selectOnTab: false, preload: null, allowEmptyOption: false, //closeAfterSelect: false, loadThrottle: 300, loadingClass: 'loading', dataAttr: null, //'data-data', optgroupField: 'optgroup', valueField: 'value', labelField: 'text', disabledField: 'disabled', optgroupLabelField: 'label', optgroupValueField: 'value', lockOptgroupOrder: false, sortField: '$order', searchField: ['text'], searchConjunction: 'and', mode: null, wrapperClass: 'ts-wrapper', controlClass: 'ts-control', dropdownClass: 'ts-dropdown', dropdownContentClass: 'ts-dropdown-content', itemClass: 'item', optionClass: 'option', dropdownParent: null, controlInput: '<input type="text" autocomplete="off" size="1" />', copyClassesToDropdown: false, placeholder: null, hidePlaceholder: null, shouldLoad: function (query) { return query.length > 0; }, /* load : null, // function(query, callback) { ... } score : null, // function(search) { ... } onInitialize : null, // function() { ... } onChange : null, // function(value) { ... } onItemAdd : null, // function(value, $item) { ... } onItemRemove : null, // function(value) { ... } onClear : null, // function() { ... } onOptionAdd : null, // function(value, data) { ... } onOptionRemove : null, // function(value) { ... } onOptionClear : null, // function() { ... } onOptionGroupAdd : null, // function(id, data) { ... } onOptionGroupRemove : null, // function(id) { ... } onOptionGroupClear : null, // function() { ... } onDropdownOpen : null, // function(dropdown) { ... } onDropdownClose : null, // function(dropdown) { ... } onType : null, // function(str) { ... } onDelete : null, // function(values) { ... } */ render: { /* item: null, optgroup: null, optgroup_header: null, option: null, option_create: null */ } }; /** * Converts a scalar to its best string representation * for hash keys and HTML attribute values. * * Transformations: * 'str' -> 'str' * null -> '' * undefined -> '' * true -> '1' * false -> '0' * 0 -> '0' * 1 -> '1' * */ const hash_key = value => { if (typeof value === 'undefined' || value === null) return null; return get_hash(value); }; const get_hash = value => { if (typeof value === 'boolean') return value ? '1' : '0'; return value + ''; }; /** * Escapes a string for use within HTML. * */ const escape_html = str => { return (str + '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }; /** * Debounce the user provided load function * */ const loadDebounce = (fn, delay) => { var timeout; return function (value, callback) { var self = this; if (timeout) { self.loading = Math.max(self.loading - 1, 0); clearTimeout(timeout); } timeout = setTimeout(function () { timeout = null; self.loadedSearches[value] = true; fn.call(self, value, callback); }, delay); }; }; /** * Debounce all fired events types listed in `types` * while executing the provided `fn`. * */ const debounce_events = (self, types, fn) => { var type; var trigger = self.trigger; var event_args = {}; // override trigger method self.trigger = function () { var type = arguments[0]; if (types.indexOf(type) !== -1) { event_args[type] = arguments; } else { return trigger.apply(self, arguments); } }; // invoke provided function fn.apply(self, []); self.trigger = trigger; // trigger queued events for (type of types) { if (type in event_args) { trigger.apply(self, event_args[type]); } } }; /** * Determines the current selection within a text input control. * Returns an object containing: * - start * - length * */ const getSelection = input => { return { start: input.selectionStart || 0, length: (input.selectionEnd || 0) - (input.selectionStart || 0) }; }; /** * Prevent default * */ const preventDefault = (evt, stop = false) => { if (evt) { evt.preventDefault(); if (stop) { evt.stopPropagation(); } } }; /** * Prevent default * */ const addEvent = (target, type, callback, options) => { target.addEventListener(type, callback, options); }; /** * Return true if the requested key is down * Will return false if more than one control character is pressed ( when [ctrl+shift+a] != [ctrl+a] ) * The current evt may not always set ( eg calling advanceSelection() ) * */ const isKeyDown = (key_name, evt) => { if (!evt) { return false; } if (!evt[key_name]) { return false; } var count = (evt.altKey ? 1 : 0) + (evt.ctrlKey ? 1 : 0) + (evt.shiftKey ? 1 : 0) + (evt.metaKey ? 1 : 0); if (count === 1) { return true; } return false; }; /** * Get the id of an element * If the id attribute is not set, set the attribute with the given id * */ const getId = (el, id) => { const existing_id = el.getAttribute('id'); if (existing_id) { return existing_id; } el.setAttribute('id', id); return id; }; /** * Returns a string with backslashes added before characters that need to be escaped. */ const addSlashes = str => { return str.replace(/[\\"']/g, '\\$&'); }; /** * */ const append = (parent, node) => { if (node) parent.append(node); }; function getSettings(input, settings_user) { var settings = Object.assign({}, defaults, settings_user); var attr_data = settings.dataAttr; var field_label = settings.labelField; var field_value = settings.valueField; var field_disabled = settings.disabledField; var field_optgroup = settings.optgroupField; var field_optgroup_label = settings.optgroupLabelField; var field_optgroup_value = settings.optgroupValueField; var tag_name = input.tagName.toLowerCase(); var placeholder = input.getAttribute('placeholder') || input.getAttribute('data-placeholder'); if (!placeholder && !settings.allowEmptyOption) { let option = input.querySelector('option[value=""]'); if (option) { placeholder = option.textContent; } } var settings_element = { placeholder: placeholder, options: [], optgroups: [], items: [], maxItems: null }; /** * Initialize from a <select> element. * */ var init_select = () => { var tagName; var options = settings_element.options; var optionsMap = {}; var group_count = 1; var readData = el => { var data = Object.assign({}, el.dataset); // get plain object from DOMStringMap var json = attr_data && data[attr_data]; if (typeof json === 'string' && json.length) { data = Object.assign(data, JSON.parse(json)); } return data; }; var addOption = (option, group) => { var value = hash_key(option.value); if (value == null) return; if (!value && !settings.allowEmptyOption) return; // if the option already exists, it's probably been // duplicated in another optgroup. in this case, push // the current group to the "optgroup" property on the // existing option so that it's rendered in both places. if (optionsMap.hasOwnProperty(value)) { if (group) { var arr = optionsMap[value][field_optgroup]; if (!arr) { optionsMap[value][field_optgroup] = group; } else if (!Array.isArray(arr)) { optionsMap[value][field_optgroup] = [arr, group]; } else { arr.push(group); } } } else { var option_data = readData(option); option_data[field_label] = option_data[field_label] || option.textContent; option_data[field_value] = option_data[field_value] || value; option_data[field_disabled] = option_data[field_disabled] || option.disabled; option_data[field_optgroup] = option_data[field_optgroup] || group; option_data.$option = option; optionsMap[value] = option_data; options.push(option_data); } if (option.selected) { settings_element.items.push(value); } }; var addGroup = optgroup => { var id, optgroup_data; optgroup_data = readData(optgroup); optgroup_data[field_optgroup_label] = optgroup_data[field_optgroup_label] || optgroup.getAttribute('label') || ''; optgroup_data[field_optgroup_value] = optgroup_data[field_optgroup_value] || group_count++; optgroup_data[field_disabled] = optgroup_data[field_disabled] || optgroup.disabled; settings_element.optgroups.push(optgroup_data); id = optgroup_data[field_optgroup_value]; iterate(optgroup.children, option => { addOption(option, id); }); }; settings_element.maxItems = input.hasAttribute('multiple') ? null : 1; iterate(input.children, child => { tagName = child.tagName.toLowerCase(); if (tagName === 'optgroup') { addGroup(child); } else if (tagName === 'option') { addOption(child); } }); }; /** * Initialize from a <input type="text"> element. * */ var init_textbox = () => { const data_raw = input.getAttribute(attr_data); if (!data_raw) { var value = input.value.trim() || ''; if (!settings.allowEmptyOption && !value.length) return; const values = value.split(settings.delimiter); iterate(values, value => { const option = {}; option[field_label] = value; option[field_value] = value; settings_element.options.push(option); }); settings_element.items = values; } else { settings_element.options = JSON.parse(data_raw); iterate(settings_element.options, opt => { settings_element.items.push(opt[field_value]); }); } }; if (tag_name === 'select') { init_select(); } else { init_textbox(); } return Object.assign({}, defaults, settings_element, settings_user); } var instance_i = 0; class TomSelect extends MicroPlugin(MicroEvent) { // @deprecated 1.8 constructor(input_arg, user_settings) { super(); this.control_input = void 0; this.wrapper = void 0; this.dropdown = void 0; this.control = void 0; this.dropdown_content = void 0; this.focus_node = void 0; this.order = 0; this.settings = void 0; this.input = void 0; this.tabIndex = void 0; this.is_select_tag = void 0; this.rtl = void 0; this.inputId = void 0; this._destroy = void 0; this.sifter = void 0; this.isOpen = false; this.isDisabled = false; this.isRequired = void 0; this.isInvalid = false; this.isValid = true; this.isLocked = false; this.isFocused = false; this.isInputHidden = false; this.isSetup = false; this.ignoreFocus = false; this.hasOptions = false; this.currentResults = void 0; this.lastValue = ''; this.caretPos = 0; this.loading = 0; this.loadedSearches = {}; this.activeOption = null; this.activeItems = []; this.optgroups = {}; this.options = {}; this.userOptions = {}; this.items = []; instance_i++; var dir; var input = getDom(input_arg); if (input.tomselect) { throw new Error('Tom Select already initialized on this element'); } input.tomselect = this; // detect rtl environment var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null); dir = computedStyle.getPropertyValue('direction'); // setup default state const settings = getSettings(input, user_settings); this.settings = settings; this.input = input; this.tabIndex = input.tabIndex || 0; this.is_select_tag = input.tagName.toLowerCase() === 'select'; this.rtl = /rtl/i.test(dir); this.inputId = getId(input, 'tomselect-' + instance_i); this.isRequired = input.required; // search system this.sifter = new Sifter(this.options, { diacritics: settings.diacritics }); // option-dependent defaults settings.mode = settings.mode || (settings.maxItems === 1 ? 'single' : 'multi'); if (typeof settings.hideSelected !== 'boolean') { settings.hideSelected = settings.mode === 'multi'; } if (typeof settings.hidePlaceholder !== 'boolean') { settings.hidePlaceholder = settings.mode !== 'multi'; } // set up createFilter callback var filter = settings.createFilter; if (typeof filter !== 'function') { if (typeof filter === 'string') { filter = new RegExp(filter); } if (filter instanceof RegExp) { settings.createFilter = input => filter.test(input); } else { settings.createFilter = value => { return this.settings.duplicates || !this.options[value]; }; } } this.initializePlugins(settings.plugins); this.setupCallbacks(); this.setupTemplates(); // Create all elements const wrapper = getDom('<div>'); const control = getDom('<div>'); const dropdown = this._render('dropdown'); const dropdown_content = getDom(`<div role="listbox" tabindex="-1">`); const classes = this.input.getAttribute('class') || ''; const inputMode = settings.mode; var control_input; addClasses(wrapper, settings.wrapperClass, classes, inputMode); addClasses(control, settings.controlClass); append(wrapper, control); addClasses(dropdown, settings.dropdownClass, inputMode); if (settings.copyClassesToDropdown) { addClasses(dropdown, classes); } addClasses(dropdown_content, settings.dropdownContentClass); append(dropdown, dropdown_content); getDom(settings.dropdownParent || wrapper).appendChild(dropdown); // default controlInput if (isHtmlString(settings.controlInput)) { control_input = getDom(settings.controlInput); // set attributes var attrs = ['autocorrect', 'autocapitalize', 'autocomplete']; iterate(attrs, attr => { if (input.getAttribute(attr)) { setAttr(control_input, { [attr]: input.getAttribute(attr) }); } }); control_input.tabIndex = -1; control.appendChild(control_input); this.focus_node = control_input; // dom element } else if (settings.controlInput) { control_input = getDom(settings.controlInput); this.focus_node = control_input; } else { control_input = getDom('<input/>'); this.focus_node = control; } this.wrapper = wrapper; this.dropdown = dropdown; this.dropdown_content = dropdown_content; this.control = control; this.control_input = control_input; this.setup(); } /** * set up event bindings. * */ setup() { const self = this; const settings = self.settings; const control_input = self.control_input; const dropdown = self.dropdown; const dropdown_content = self.dropdown_content; const wrapper = self.wrapper; const control = self.control; const input = self.input; const focus_node = self.focus_node; const passive_event = { passive: true }; const listboxId = self.inputId + '-ts-dropdown'; setAttr(dropdown_content, { id: listboxId }); setAttr(focus_node, { role: 'combobox', 'aria-haspopup': 'listbox', 'aria-expanded': 'false', 'aria-controls': listboxId }); const control_id = getId(focus_node, self.inputId + '-ts-control'); const query = "label[for='" + escapeQuery(self.inputId) + "']"; const label = document.querySelector(query); const label_click = self.focus.bind(self); if (label) { addEvent(label, 'click', label_click); setAttr(label, { for: control_id }); const label_id = getId(label, self.inputId + '-ts-label'); setAttr(focus_node, { 'aria-labelledby': label_id }); setAttr(dropdown_content, { 'aria-labelledby': label_id }); } wrapper.style.width = input.style.width; if (self.plugins.names.length) { const classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-'); addClasses([wrapper, dropdown], classes_plugins); } if ((settings.maxItems === null || settings.maxItems > 1) && self.is_select_tag) { setAttr(input, { multiple: 'multiple' }); } if (settings.placeholder) { setAttr(control_input, { placeholder: settings.placeholder }); } // if splitOn was not passed in, construct it from the delimiter to allow pasting universally if (!settings.splitOn && settings.delimiter) { settings.splitOn = new RegExp('\\s*' + escape_regex(settings.delimiter) + '+\\s*'); } // debounce user defined load() if loadThrottle > 0 // after initializePlugins() so plugins can create/modify user defined loaders if (settings.load && settings.loadThrottle) { settings.load = loadDebounce(settings.load, settings.loadThrottle); } self.control_input.type = input.type; // clicking on an option should select it addEvent(dropdown, 'click', evt => { const option = parentMatch(evt.target, '[data-selectable]'); if (option) { self.onOptionSelect(evt, option); preventDefault(evt, true); } }); addEvent(control, 'click', evt => { var target_match = parentMatch(evt.target, '[data-ts-item]', control); if (target_match && self.onItemSelect(evt, target_match)) { preventDefault(evt, true); return; } // retain focus (see control_input mousedown) if (control_input.value != '') { return; } self.onClick(); preventDefault(evt, true); }); // keydown on focus_node for arrow_down/arrow_up addEvent(focus_node, 'keydown', e => self.onKeyDown(e)); // keypress and input/keyup addEvent(control_input, 'keypress', e => self.onKeyPress(e)); addEvent(control_input, 'input', e => self.onInput(e)); addEvent(focus_node, 'resize', () => self.positionDropdown(), passive_event); addEvent(focus_node, 'blur', e => self.onBlur(e)); addEvent(focus_node, 'focus', e => self.onFocus(e)); addEvent(focus_node, 'paste', e => self.onPaste(e)); const doc_mousedown = evt => { // blur if target is outside of this instance // dropdown is not always inside wrapper const target = evt.composedPath()[0]; if (!wrapper.contains(target) && !dropdown.contains(target)) { if (self.isFocused) { self.blur(); } self.inputState(); return; } // retain focus by preventing native handling. if the // event target is the input it should not be modified. // otherwise, text selection within the input won't work. // Fixes bug #212 which is no covered by tests if (target == control_input && self.isOpen) { evt.stopPropagation(); // clicking anywhere in the control should not blur the control_input (which would close the dropdown) } else { preventDefault(evt, true); } }; var win_scroll = () => { if (self.isOpen) { self.positionDropdown(); } }; addEvent(document, 'mousedown', doc_mousedown); addEvent(window, 'scroll', win_scroll, passive_event); addEvent(window, 'resize', win_scroll, passive_event); this._destroy = () => { document.removeEventListener('mousedown', doc_mousedown); window.removeEventListener('scroll', win_scroll); window.removeEventListener('resize', win_scroll); if (label) label.removeEventListener('click', label_click); }; // store original html and tab index so that they can be // restored when the destroy() method is called. this.revertSettings = { innerHTML: input.innerHTML, tabIndex: input.tabIndex }; input.tabIndex = -1; input.insertAdjacentElement('afterend', self.wrapper); self.sync(false); settings.items = []; delete settings.optgroups; delete settings.options; addEvent(input, 'invalid', e => { if (self.isValid) { self.isValid = false; self.isInvalid = true; self.refreshState(); } }); self.updateOriginalInput(); self.refreshItems(); self.close(false); self.inputState(); self.isSetup = true; if (input.disabled) { self.disable(); } else { self.enable(); //sets tabIndex } self.on('change', this.onChange); addClasses(input, 'tomselected', 'ts-hidden-accessible'); self.trigger('initialize'); // preload options if (settings.preload === true) { self.preload(); } } /** * Register options and optgroups * */ setupOptions(options = [], optgroups = []) { // build options table this.addOptions(options); // build optgroup table iterate(optgroups, optgroup => { this.registerOptionGroup(optgroup); }); } /** * Sets up default rendering functions. */ setupTemplates() { var self = this; var field_label = self.settings.labelField; var field_optgroup = self.settings.optgroupLabelField; var templates = { 'optgroup': data => { let optgroup = document.createElement('div'); optgroup.className = 'optgroup'; optgroup.appendChild(data.options); return optgroup; }, 'optgroup_header': (data, escape) => { return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>'; }, 'option': (data, escape) => { return '<div>' + escape(data[field_label]) + '</div>'; }, 'item': (data, escape) => { return '<div>' + escape(data[field_label]) + '</div>'; }, 'option_create': (data, escape) => { return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>'; }, 'no_results': () => { return '<div class="no-results">No results found</div>'; }, 'loading': () => { return '<div class="spinner"></div>'; }, 'not_loading': () => {}, 'dropdown': () => { return '<div></div>'; } }; self.settings.render = Object.assign({}, templates, self.settings.render); } /** * Maps fired events to callbacks provided * in the settings used when creating the control. */ setupCallbacks() { var key, fn; var callbacks = { 'initialize': 'onInitialize', 'change': 'onChange', 'item_add': 'onItemAdd', 'item_remove': 'onItemRemove', 'item_select': 'onItemSelect', 'clear': 'onClear', 'option_add': 'onOptionAdd', 'option_remove': 'onOptionRemove', 'option_clear': 'onOptionClear', 'optgroup_add': 'onOptio