From 34276111628dd2588a2b662f646bad1d52e3474a Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 2 Feb 2026 22:06:58 +0100 Subject: [PATCH] Tom Select lib added. see #603 --- VENDORS.md | 3 + pages/_lib/tom-select.base.js | 4135 +++++++++++++++++++++++++++++++++ 2 files changed, 4138 insertions(+) create mode 100644 pages/_lib/tom-select.base.js diff --git a/VENDORS.md b/VENDORS.md index e58bb72e..c47d117d 100644 --- a/VENDORS.md +++ b/VENDORS.md @@ -1,5 +1,8 @@ file: pages\_lib\list.js source: https://raw.githubusercontent.com/javve/list.js/v2.3.1/dist/list.js +file: pages\_lib\tom-select.base.js +source: https://cdn.jsdelivr.net/npm/tom-select@2.4.4/dist/js/tom-select.base.js + file: js\lib\diff.js source: https://cdnjs.cloudflare.com/ajax/libs/jsdiff/7.0.0/diff.js \ No newline at end of file diff --git a/pages/_lib/tom-select.base.js b/pages/_lib/tom-select.base.js new file mode 100644 index 00000000..3a36e988 --- /dev/null +++ b/pages/_lib/tom-select.base.js @@ -0,0 +1,4135 @@ +/** +* Tom Select v2.4.4 +* 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,