/** * Tom Select v2.4.3 * Licensed under the Apache License, Version 2.0 (the "License"); */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TomSelect = factory()); })(this, (function () { '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 = {}; } on(events, fct) { forEvents(events, event => { const event_array = this._events[event] || []; event_array.push(fct); this._events[event] = event_array; }); } off(events, fct) { var n = arguments.length; if (n === 0) { this._events = {}; return; } forEvents(events, event => { if (n === 1) { delete this._events[event]; return; } const event_array = this._events[event]; if (event_array === undefined) return; event_array.splice(event_array.indexOf(fct), 1); this._events[event] = event_array; }); } trigger(events, ...args) { var self = this; forEvents(events, event => { const event_array = self._events[event]; if (event_array === undefined) return; event_array.forEach(fct => { 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 */ 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]; } }; } /** * Convert array of strings to a regular expression * ex ['ab','a'] => (?:ab|a) * ex ['a','b'] => [ab] */ const arrayToPattern = (chars) => { chars = chars.filter(Boolean); if (chars.length < 2) { return chars[0] || ''; } return (maxValueLength(chars) == 1) ? '[' + chars.join('') + ']' : '(?:' + chars.join('|') + ')'; }; const sequencePattern = (array) => { if (!hasDuplicates(array)) { return array.join(''); } let pattern = ''; let prev_char_count = 0; const prev_pattern = () => { if (prev_char_count > 1) { pattern += '{' + prev_char_count + '}'; } }; array.forEach((char, i) => { if (char === array[i - 1]) { prev_char_count++; return; } prev_pattern(); pattern += char; prev_char_count = 1; }); prev_pattern(); return pattern; }; /** * Convert array of strings to a regular expression * ex ['ab','a'] => (?:ab|a) * ex ['a','b'] => [ab] */ const setToPattern = (chars) => { let array = Array.from(chars); return arrayToPattern(array); }; /** * https://stackoverflow.com/questions/7376598/in-javascript-how-do-i-check-if-an-array-has-duplicate-values */ const hasDuplicates = (array) => { return (new Set(array)).size !== array.length; }; /** * https://stackoverflow.com/questions/63006601/why-does-u-throw-an-invalid-escape-error */ const escape_regex = (str) => { return (str + '').replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu, '\\$1'); }; /** * Return the max length of array values */ const maxValueLength = (array) => { return array.reduce((longest, value) => Math.max(longest, unicodeLength(value)), 0); }; const unicodeLength = (str) => { return Array.from(str).length; }; /** * 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]]; let result = []; const start = input.substring(1); const suba = allSubstrings(start); suba.forEach(function (subresult) { let 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; }; const code_points = [[0, 65535]]; const accent_pat = '[\u0300-\u036F\u{b7}\u{2be}\u{2bc}]'; let unicode_map; let multi_char_reg; const max_char_length = 3; const latin_convert = {}; const latin_condensed = { '/': '⁄∕', '0': '߀', "a": "ⱥɐɑ", "aa": "ꜳ", "ae": "æǽǣ", "ao": "ꜵ", "au": "ꜷ", "av": "ꜹꜻ", "ay": "ꜽ", "b": "ƀɓƃ", "c": "ꜿƈȼↄ", "d": "đɗɖᴅƌꮷԁɦ", "e": "ɛǝᴇɇ", "f": "ꝼƒ", "g": "ǥɠꞡᵹꝿɢ", "h": "ħⱨⱶɥ", "i": "ɨı", "j": "ɉȷ", "k": "ƙⱪꝁꝃꝅꞣ", "l": "łƚɫⱡꝉꝇꞁɭ", "m": "ɱɯϻ", "n": "ꞥƞɲꞑᴎлԉ", "o": "øǿɔɵꝋꝍᴑ", "oe": "œ", "oi": "ƣ", "oo": "ꝏ", "ou": "ȣ", "p": "ƥᵽꝑꝓꝕρ", "q": "ꝗꝙɋ", "r": "ɍɽꝛꞧꞃ", "s": "ßȿꞩꞅʂ", "t": "ŧƭʈⱦꞇ", "th": "þ", "tz": "ꜩ", "u": "ʉ", "v": "ʋꝟʌ", "vy": "ꝡ", "w": "ⱳ", "y": "ƴɏỿ", "z": "ƶȥɀⱬꝣ", "hv": "ƕ" }; for (let latin in latin_condensed) { let unicode = latin_condensed[latin] || ''; for (let i = 0; i < unicode.length; i++) { let char = unicode.substring(i, i + 1); latin_convert[char] = latin; } } const convert_pat = new RegExp(Object.keys(latin_convert).join('|') + '|' + accent_pat, 'gu'); /** * Initialize the unicode_map from the give code point ranges */ const initialize = (_code_points) => { if (unicode_map !== undefined) return; unicode_map = generateMap(code_points); }; /** * Helper method for normalize a string * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize */ const normalize = (str, form = 'NFKD') => str.normalize(form); /** * Remove accents without reordering string * calling str.normalize('NFKD') on \u{594}\u{595}\u{596} becomes \u{596}\u{594}\u{595} * via https://github.com/krisk/Fuse/issues/133#issuecomment-318692703 */ const asciifold = (str) => { return Array.from(str).reduce( /** * @param {string} result * @param {string} char */ (result, char) => { return result + _asciifold(char); }, ''); }; const _asciifold = (str) => { str = normalize(str) .toLowerCase() .replace(convert_pat, (/** @type {string} */ char) => { return latin_convert[char] || ''; }); //return str; return normalize(str, 'NFC'); }; /** * Generate a list of unicode variants from the list of code points */ function* generator(code_points) { for (const [code_point_min, code_point_max] of code_points) { for (let i = code_point_min; i <= code_point_max; i++) { let composed = String.fromCharCode(i); let folded = asciifold(composed); if (folded == composed.toLowerCase()) { continue; } // skip when folded is a string longer than 3 characters long // bc the resulting regex patterns will be long // eg: // folded صلى الله عليه وسلم length 18 code point 65018 // folded جل جلاله length 8 code point 65019 if (folded.length > max_char_length) { continue; } if (folded.length == 0) { continue; } yield { folded: folded, composed: composed, code_point: i }; } } } /** * Generate a unicode map from the list of code points */ const generateSets = (code_points) => { const unicode_sets = {}; const addMatching = (folded, to_add) => { /** @type {Set} */ const folded_set = unicode_sets[folded] || new Set(); const patt = new RegExp('^' + setToPattern(folded_set) + '$', 'iu'); if (to_add.match(patt)) { return; } folded_set.add(escape_regex(to_add)); unicode_sets[folded] = folded_set; }; for (let value of generator(code_points)) { addMatching(value.folded, value.folded); addMatching(value.folded, value.composed); } return unicode_sets; }; /** * Generate a unicode map from the list of code points * ae => (?:(?:ae|Æ|Ǽ|Ǣ)|(?:A|Ⓐ|A...)(?:E|ɛ|Ⓔ...)) */ const generateMap = (code_points) => { const unicode_sets = generateSets(code_points); const unicode_map = {}; let multi_char = []; for (let folded in unicode_sets) { let set = unicode_sets[folded]; if (set) { unicode_map[folded] = setToPattern(set); } if (folded.length > 1) { multi_char.push(escape_regex(folded)); } } multi_char.sort((a, b) => b.length - a.length); const multi_char_patt = arrayToPattern(multi_char); multi_char_reg = new RegExp('^' + multi_char_patt, 'u'); return unicode_map; }; /** * Map each element of an array from its folded value to all possible unicode matches */ const mapSequence = (strings, min_replacement = 1) => { let chars_replaced = 0; strings = strings.map((str) => { if (unicode_map[str]) { chars_replaced += str.length; } return unicode_map[str] || str; }); if (chars_replaced >= min_replacement) { return sequencePattern(strings); } return ''; }; /** * Convert a short string and split it into all possible patterns * Keep a pattern only if min_replacement is met * * 'abc' * => [['abc'],['ab','c'],['a','bc'],['a','b','c']] * => ['abc-pattern','ab-c-pattern'...] */ const substringsToPattern = (str, min_replacement = 1) => { min_replacement = Math.max(min_replacement, str.length - 1); return arrayToPattern(allSubstrings(str).map((sub_pat) => { return mapSequence(sub_pat, min_replacement); })); }; /** * Convert an array of sequences into a pattern * [{start:0,end:3,length:3,substr:'iii'}...] => (?:iii...) */ const sequencesToPattern = (sequences, all = true) => { let min_replacement = sequences.length > 1 ? 1 : 0; return arrayToPattern(sequences.map((sequence) => { let seq = []; const len = all ? sequence.length() : sequence.length() - 1; for (let j = 0; j < len; j++) { seq.push(substringsToPattern(sequence.substrs[j] || '', min_replacement)); } return sequencePattern(seq); })); }; /** * Return true if the sequence is already in the sequences */ const inSequences = (needle_seq, sequences) => { for (const seq of sequences) { if (seq.start != needle_seq.start || seq.end != needle_seq.end) { continue; } if (seq.substrs.join('') !== needle_seq.substrs.join('')) { continue; } let needle_parts = needle_seq.parts; const filter = (part) => { for (const needle_part of needle_parts) { if (needle_part.start === part.start && needle_part.substr === part.substr) { return false; } if (part.length == 1 || needle_part.length == 1) { continue; } // check for overlapping parts // a = ['::=','=='] // b = ['::','==='] // a = ['r','sm'] // b = ['rs','m'] if (part.start < needle_part.start && part.end > needle_part.start) { return true; } if (needle_part.start < part.start && needle_part.end > part.start) { return true; } } return false; }; let filtered = seq.parts.filter(filter); if (filtered.length > 0) { continue; } return true; } return false; }; class Sequence { parts; substrs; start; end; constructor() { this.parts = []; this.substrs = []; this.start = 0; this.end = 0; } add(part) { if (part) { this.parts.push(part); this.substrs.push(part.substr); this.start = Math.min(part.start, this.start); this.end = Math.max(part.end, this.end); } } last() { return this.parts[this.parts.length - 1]; } length() { return this.parts.length; } clone(position, last_piece) { let clone = new Sequence(); let parts = JSON.parse(JSON.stringify(this.parts)); let last_part = parts.pop(); for (const part of parts) { clone.add(part); } let last_substr = last_piece.substr.substring(0, position - last_part.start); let clone_last_len = last_substr.length; clone.add({ start: last_part.start, end: last_part.start + clone_last_len, length: clone_last_len, substr: last_substr }); return clone; } } /** * Expand a regular expression pattern to include unicode variants * eg /a/ becomes /aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐɑAⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ/ * * Issue: * ﺊﺋ [ 'ﺊ = \\u{fe8a}', 'ﺋ = \\u{fe8b}' ] * becomes: ئئ [ 'ي = \\u{64a}', 'ٔ = \\u{654}', 'ي = \\u{64a}', 'ٔ = \\u{654}' ] * * İIJ = IIJ = ⅡJ * * 1/2/4 */ const getPattern = (str) => { initialize(); str = asciifold(str); let pattern = ''; let sequences = [new Sequence()]; for (let i = 0; i < str.length; i++) { let substr = str.substring(i); let match = substr.match(multi_char_reg); const char = str.substring(i, i + 1); const match_str = match ? match[0] : null; // loop through sequences // add either the char or multi_match let overlapping = []; let added_types = new Set(); for (const sequence of sequences) { const last_piece = sequence.last(); if (!last_piece || last_piece.length == 1 || last_piece.end <= i) { // if we have a multi match if (match_str) { const len = match_str.length; sequence.add({ start: i, end: i + len, length: len, substr: match_str }); added_types.add('1'); } else { sequence.add({ start: i, end: i + 1, length: 1, substr: char }); added_types.add('2'); } } else if (match_str) { let clone = sequence.clone(i, last_piece); const len = match_str.length; clone.add({ start: i, end: i + len, length: len, substr: match_str }); overlapping.push(clone); } else { // don't add char // adding would create invalid patterns: 234 => [2,34,4] added_types.add('3'); } } // if we have overlapping if (overlapping.length > 0) { // ['ii','iii'] before ['i','i','iii'] overlapping = overlapping.sort((a, b) => { return a.length() - b.length(); }); for (let clone of overlapping) { // don't add if we already have an equivalent sequence if (inSequences(clone, sequences)) { continue; } sequences.push(clone); } continue; } // if we haven't done anything unique // clean up the patterns // helps keep patterns smaller // if str = 'r₨㎧aarss', pattern will be 446 instead of 655 if (i > 0 && added_types.size == 1 && !added_types.has('3')) { pattern += sequencesToPattern(sequences, false); let new_seq = new Sequence(); const old_seq = sequences[0]; if (old_seq) { new_seq.add(old_seq.last()); } sequences = [new_seq]; } } pattern += sequencesToPattern(sequences, true); return pattern; }; /** * 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 + ''; if (token.regex == null) return 0; 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; }; /** * 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$1 = (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 */ class Sifter { items; // []|{}; settings; /** * Textually searches arrays and hashes of objects * by property (or multiple properties). Designed * specifically for autocomplete. * */ constructor(items, settings) { 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) { if (this.settings.diacritics) { regex = getPattern(word) || null; } else { regex = escape_regex(word); } if (regex && 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 {T.ScoreFn} */ getScoreFunction(query, options) { var search = this.prepareSearch(query, options); return this._getScoreFunction(search); } /** * @returns {T.ScoreFn} * */ _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] || 1); }; } 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$1(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 score, sum = 0; for (let token of tokens) { score = scoreObject(token, data); if (score <= 0) return 0; sum += score; } return sum / token_count; }; } else { return function (data) { var sum = 0; iterate$1(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 implicit_score, sort_flds = []; const self = this, options = search.options, sort = (!search.query && options.sort_empty) ? options.sort_empty : options.sort; if (typeof sort == 'function') { return sort.bind(this); } /** * Fetches the specified sort field value * from a search result item. * */ const get_field = function (name, result) { if (name === '$score') return result.score; return search.getAttrFn(self.items[result.id], name); }; // parse options if (sort) { for (let s of sort) { if (search.query || s.field !== '$score') { sort_flds.push(s); } } } // the "$score" field is implied to be the primary // sort field, unless it's manually specified if (search.query) { implicit_score = true; for (let fld of sort_flds) { if (fld.field === '$score') { implicit_score = false; break; } } if (implicit_score) { sort_flds.unshift({ field: '$score', direction: 'desc' }); } // without a search.query, all items will have the same score } else { sort_flds = sort_flds.filter((fld) => fld.field !== '$score'); } // build function const sort_flds_count = sort_flds.length; if (!sort_flds_count) { return null; } return function (a, b) { var result, field; for (let sort_fld of sort_flds) { field = sort_fld.field; let multiplier = sort_fld.direction === 'desc' ? -1 : 1; result = multiplier * 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$1(self.items, (item, id) => { score = fn_score(item); if (options.filter === false || score > 0) { search.items.push({ 'score': score, 'id': id }); } }); } else { iterate$1(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; } ; } /** * 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, '&').replace(//g, '>').replace(/"/g, '"'); }; /** * use setTimeout if timeout > 0 */ const timeout = (fn, timeout) => { if (timeout > 0) { return window.setTimeout(fn, timeout); } fn.call(null); return null; }; /** * 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 * * Note: "selectionStart, selectionEnd ... apply only to inputs of types text, search, URL, tel and password" * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange */ 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(); } } }; /** * Add event helper * */ 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); }; /** * 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); } } } }; /** * 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)) { var tpl = document.createElement('template'); tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result return tpl.content.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(/[\t\n\f\r\s]/); } 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 * Highlights arbitrary terms in a node. * * - Modified by Marshal 2011-6-24 (added regex) * - Modified by Brian Reavis 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 , e.g. // Soccer -> Soccer 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,