tom-select
Version:
Tom Select is a versatile and dynamic <select> UI control. Forked from Selectize.js to provide a framework agnostic autocomplete widget with native-feeling keyboard navigation, it's useful for tagging, contact lists, country selectors, etc.
1,686 lines (1,632 loc) • 131 kB
JavaScript
/**
* Tom Select v2.5.2
* 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 <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];
}
};
}
/**
* 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<string>} */
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 <brian@thirdroute.com>
*/
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, '>').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 <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')) {
Array.from(node.childNodes).forEach(element => {
highlightRecursive(element);
});
}
};
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,
clearAfterSelect: false,
highlight: true,
openOnFocus: true,
shouldOpen: null,
maxOptions: 50,
maxItems: null,
hideSelected: null,
duplicates: false,
addPrecedence: false,
selectOnTab: false,
preload: null,
allowEmptyOption: false,
//closeAfterSelect: false,
refreshThrottle: 300,
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
*/
}
};
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;
let $order = 0;
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;
option_data.$order = option_data.$order || ++$order;
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;
optgroup_data.$order = optgroup_data.$order || ++$order;
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