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/js/mzta-utils.js b/js/mzta-utils.js
index 08106d00..d032c4da 100644
--- a/js/mzta-utils.js
+++ b/js/mzta-utils.js
@@ -523,6 +523,14 @@ export async function transformTagsLabels(labels, tags_list) {
return output;
}
+export function setTomSelectBorder(el){
+ if (el.getValue() === "") {
+ el.control.style.border = '2px solid red';
+ } else {
+ el.control.style.border = '1px solid #d0d0d0';
+ }
+}
+
export function getAPIsInitMessageString(args = {}) {
const {
api_string = '',
diff --git a/options/mzta-options.js b/options/mzta-options.js
index 10d09874..9a085532 100644
--- a/options/mzta-options.js
+++ b/options/mzta-options.js
@@ -25,6 +25,7 @@ import {
getChatGPTWebModelsList_HTML,
isAPIKeyValue,
getConnectionType,
+ setTomSelectBorder
} from '../js/mzta-utils.js';
import {
injectConnectionUI,
@@ -75,6 +76,7 @@ function saveOptions(e) {
async function restoreOptions() {
function setCurrentChoice(result) {
document.querySelectorAll(".option-input").forEach(element => {
+ if(!element.id) return;
taLog.log("Options restoring " + element.id + " = " + (isAPIKeyValue(element.id) ? "****************" : result[element.id]));
switch (element.type) {
case 'checkbox':
@@ -100,6 +102,10 @@ async function restoreOptions() {
if (element.value === '') {
element.selectedIndex = -1;
}
+ if (element.tomselect) {
+ element.tomselect.setValue(element.value, true);
+ setTomSelectBorder(element.tomselect);
+ }
break;
case 'textarea':
element.value = result[element.id];
diff --git a/pages/_lib/connection-ui.css b/pages/_lib/connection-ui.css
index 1f4a3263..1c2912c8 100644
--- a/pages/_lib/connection-ui.css
+++ b/pages/_lib/connection-ui.css
@@ -1,3 +1,5 @@
+@import url("tom-select.default.min.css");
+
tr.conntype_chatgpt_api, tr.conntype_chatgpt_api2{
background-color: rgb(185, 220, 252) !important;
}
@@ -91,6 +93,10 @@ span.opt_title{
font-weight: bold;
}
+.ts-control, .ts-dropdown{
+ font-size: 14.4px;
+}
+
@media (prefers-color-scheme: dark) {
tr.conntype_chatgpt_api, tr.conntype_chatgpt_api2{
background-color: rgb(0, 0, 58) !important;
diff --git a/pages/_lib/connection-ui.js b/pages/_lib/connection-ui.js
index bd52abf4..edc6f656 100644
--- a/pages/_lib/connection-ui.js
+++ b/pages/_lib/connection-ui.js
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import './tom-select.base.js';
import {
prefs_default,
integration_options_config
@@ -29,7 +30,8 @@ import {
validateCustomData_ChatGPTWeb,
sanitizeChatGPTModelData,
sanitizeChatGPTWebCustomData,
- prepareOriginURL
+ prepareOriginURL,
+ setTomSelectBorder
} from '../../js/mzta-utils.js';
import { openAICompConfigs } from '../../js/api/openai_comp_configs.js';
import {
@@ -669,6 +671,7 @@ export async function injectConnectionUI({
while (openaiCompModelSelect.options.length > 0) {
openaiCompModelSelect.remove(0);
}
+ syncTomSelect(openaiCompModelSelect);
document.getElementById(getPrefixedId('openai_comp_use_v1')).checked = !!config.use_v1;
document.getElementById(getPrefixedId('openai_comp_chat_name')).value = config.chat_name || '';
// Trigger change events if needed
@@ -697,6 +700,7 @@ export async function injectConnectionUI({
chatgpt_option.value = prefs.chatgpt_model;
chatgpt_option.text = prefs.chatgpt_model;
select_chatgpt_model.appendChild(chatgpt_option);
+ select_chatgpt_model.value = prefs.chatgpt_model;
select_chatgpt_model.addEventListener("change", () => warn_ChatGPT_APIKeyEmpty(modelId_prefix));
document.getElementById(getPrefixedId('btnUpdateChatGPTModels')).addEventListener('click', async () => {
@@ -734,6 +738,7 @@ export async function injectConnectionUI({
select_chatgpt_model.appendChild(option);
}
});
+ syncTomSelect(select_chatgpt_model);
document.getElementById(getPrefixedId('chatgpt_model_fetch_loading')).style.display = 'none';
});
@@ -746,6 +751,7 @@ export async function injectConnectionUI({
google_gemini_option.value = prefs.google_gemini_model;
google_gemini_option.text = prefs.google_gemini_model;
select_google_gemini_model.appendChild(google_gemini_option);
+ select_google_gemini_model.value = prefs.google_gemini_model;
select_google_gemini_model.addEventListener("change", () => warn_GoogleGemini_APIKeyEmpty(modelId_prefix));
document.getElementById(getPrefixedId('btnUpdateGoogleGeminiModels')).addEventListener('click', async () => {
@@ -776,6 +782,7 @@ export async function injectConnectionUI({
select_google_gemini_model.appendChild(option);
}
});
+ syncTomSelect(select_google_gemini_model);
document.getElementById(getPrefixedId('google_gemini_model_fetch_loading')).style.display = 'none';
});
@@ -788,6 +795,7 @@ export async function injectConnectionUI({
ollama_option.value = prefs.ollama_model;
ollama_option.text = prefs.ollama_model;
select_ollama_model.appendChild(ollama_option);
+ select_ollama_model.value = prefs.ollama_model;
select_ollama_model.addEventListener("change", () => warn_Ollama_HostEmpty(modelId_prefix));
document.getElementById(getPrefixedId('btnUpdateOllamaModels')).addEventListener('click', async () => {
@@ -831,6 +839,7 @@ export async function injectConnectionUI({
select_ollama_model.appendChild(option);
}
});
+ syncTomSelect(select_ollama_model);
document.getElementById(getPrefixedId('ollama_model_fetch_loading')).style.display = 'none';
} catch (error) {
document.getElementById(getPrefixedId('ollama_model_fetch_loading')).style.display = 'none';
@@ -847,6 +856,7 @@ export async function injectConnectionUI({
openai_comp_option.value = prefs.openai_comp_model;
openai_comp_option.text = prefs.openai_comp_model;
select_openai_comp_model.appendChild(openai_comp_option);
+ select_openai_comp_model.value = prefs.openai_comp_model;
select_openai_comp_model.addEventListener("change", () => warn_OpenAIComp_HostEmpty(modelId_prefix));
document.getElementById(getPrefixedId('btnUpdateOpenAICompModels')).addEventListener('click', async () => {
@@ -879,6 +889,7 @@ export async function injectConnectionUI({
select_openai_comp_model.appendChild(option);
}
});
+ syncTomSelect(select_openai_comp_model);
document.getElementById(getPrefixedId('openai_comp_model_fetch_loading')).style.display = 'none';
});
@@ -891,6 +902,7 @@ export async function injectConnectionUI({
anthropic_option.value = prefs.anthropic_model;
anthropic_option.text = prefs.anthropic_model;
select_anthropic_model.appendChild(anthropic_option);
+ select_anthropic_model.value = prefs.anthropic_model;
select_anthropic_model.addEventListener("change", () => warn_Anthropic_APIKeyEmpty(modelId_prefix));
select_anthropic_model.addEventListener("change", () => warn_Anthropic_VersionEmpty(modelId_prefix));
@@ -933,6 +945,7 @@ export async function injectConnectionUI({
select_anthropic_model.appendChild(option);
}
});
+ syncTomSelect(select_anthropic_model);
document.getElementById(getPrefixedId('anthropic_model_fetch_loading')).style.display = 'none';
});
@@ -948,6 +961,7 @@ export async function injectConnectionUI({
option.text = modelName;
select_openai_comp_model.appendChild(option);
select_openai_comp_model.value = modelName;
+ syncTomSelect(select_openai_comp_model);
select_openai_comp_model.dispatchEvent(new Event('change', { bubbles: true }));
}
});
@@ -961,6 +975,7 @@ export async function injectConnectionUI({
select_openai_comp_model.remove(0);
}
select_openai_comp_model.value = '';
+ syncTomSelect(select_openai_comp_model);
select_openai_comp_model.dispatchEvent(new Event('change', { bubbles: true }));
});
@@ -1011,6 +1026,29 @@ export async function injectConnectionUI({
updateCORSWarnings(modelId_prefix);
+ ['chatgpt_model', 'google_gemini_model', 'ollama_model', 'openai_comp_model', 'anthropic_model'].forEach(id => {
+ const el = document.getElementById(getPrefixedId(id));
+ if (el && !el.tomselect) {
+ let ts = new TomSelect(el, {
+ create: false,
+ maxOptions: null,
+ maxItems: 1,
+ sortField: {
+ field: "text",
+ direction: "asc"
+ }
+ });
+ ts.on('change', function() {
+ setTomSelectBorder(this);
+ });
+ if (el.value) {
+ ts.setValue(el.value);
+ }
+ console.log(">>>>>>>>>>>>>>> el.value: " + el.value);
+ setTomSelectBorder(ts);
+ }
+ });
+
return {
select: conntype_select,
onTypeChange: (fn) => { onTypeChange = fn; fn(conntype_select.value, { select: conntype_select }); }
@@ -1207,6 +1245,24 @@ export function showConnectionOptions(conntype_select, modelId_prefix = '') {
// From here there are internal functions
+function syncTomSelect(element) {
+ if (element && element.tomselect) {
+ element.tomselect.sync();
+ }
+}
+
+function toggleTomSelectDisabled(element, disabled) {
+ element.disabled = disabled;
+ if (element.tomselect) {
+ if (disabled) {
+ element.tomselect.disable();
+ element.tomselect.clear();
+ } else {
+ element.tomselect.enable();
+ }
+ }
+}
+
function getModelEl(model, modelId_prefix) {
return document.getElementById((modelId_prefix ? modelId_prefix : '') + model);
}
@@ -1261,13 +1317,13 @@ function warn_ChatGPT_APIKeyEmpty(modelId_prefix) {
if(apiKeyInput.value === ''){
apiKeyInput.style.border = '2px solid red';
btnFetchChatGPTModels.disabled = true;
- modelChatGPT.disabled = true;
+ toggleTomSelectDisabled(modelChatGPT, true);
modelChatGPT.selectedIndex = -1;
modelChatGPT.style.border = '';
}else{
apiKeyInput.style.border = '';
btnFetchChatGPTModels.disabled = false;
- modelChatGPT.disabled = false;
+ toggleTomSelectDisabled(modelChatGPT, false);
if((modelChatGPT.selectedIndex === -1)||(modelChatGPT.value === '')){
modelChatGPT.style.border = '2px solid red';
}else{
@@ -1284,13 +1340,13 @@ function warn_GoogleGemini_APIKeyEmpty(modelId_prefix) {
if(apiKeyInput.value === ''){
apiKeyInput.style.border = '2px solid red';
btnFetchGoogleGeminiModels.disabled = true;
- modelGoogleGemini.disabled = true;
+ toggleTomSelectDisabled(modelGoogleGemini, true);
modelGoogleGemini.selectedIndex = -1;
modelGoogleGemini.style.border = '';
}else{
apiKeyInput.style.border = '';
btnFetchGoogleGeminiModels.disabled = false;
- modelGoogleGemini.disabled = false;
+ toggleTomSelectDisabled(modelGoogleGemini, false);
if((modelGoogleGemini.selectedIndex === -1)||(modelGoogleGemini.value === '')){
modelGoogleGemini.style.border = '2px solid red';
}else{
@@ -1308,14 +1364,14 @@ function warn_Ollama_HostEmpty(modelId_prefix) {
if(hostInput.value === ''){
hostInput.style.border = '2px solid red';
btnFetchOllamaModels.disabled = true;
- modelOllama.disabled = true;
+ toggleTomSelectDisabled(modelOllama, true);
modelOllama.selectedIndex = -1;
modelOllama.style.border = '';
btnGiveAllUrlsPermission_ollama_api.disabled = true;
}else{
hostInput.style.border = '';
btnFetchOllamaModels.disabled = false;
- modelOllama.disabled = false;
+ toggleTomSelectDisabled(modelOllama, false);
if((modelOllama.selectedIndex === -1)||(modelOllama.value === '')){
modelOllama.style.border = '2px solid red';
}else{
@@ -1334,14 +1390,14 @@ function warn_OpenAIComp_HostEmpty(modelId_prefix) {
if(hostInput.value === ''){
hostInput.style.border = '2px solid red';
btnUpdateOpenAICompModels.disabled = true;
- modelOpenAIComp.disabled = true;
+ toggleTomSelectDisabled(modelOpenAIComp, true);
modelOpenAIComp.selectedIndex = -1;
modelOpenAIComp.style.border = '';
btnGiveAllUrlsPermission_openai_comp_api.disabled = true;
}else{
hostInput.style.border = '';
btnUpdateOpenAICompModels.disabled = false;
- modelOpenAIComp.disabled = false;
+ toggleTomSelectDisabled(modelOpenAIComp, false);
if((modelOpenAIComp.selectedIndex === -1)||(modelOpenAIComp.value === '')){
modelOpenAIComp.style.border = '2px solid red';
}else{
@@ -1359,13 +1415,13 @@ function warn_Anthropic_APIKeyEmpty(modelId_prefix) {
if(apiKeyInput.value === ''){
apiKeyInput.style.border = '2px solid red';
btnFetchAnthropicModels.disabled = true;
- modelAnthropic.disabled = true;
+ toggleTomSelectDisabled(modelAnthropic, true);
modelAnthropic.selectedIndex = -1;
modelAnthropic.style.border = '';
}else{
apiKeyInput.style.border = '';
btnFetchAnthropicModels.disabled = false;
- modelAnthropic.disabled = false;
+ toggleTomSelectDisabled(modelAnthropic, false);
if((modelAnthropic.selectedIndex === -1)||(modelAnthropic.value === '')){
modelAnthropic.style.border = '2px solid red';
}else{
@@ -1382,13 +1438,13 @@ function warn_Anthropic_VersionEmpty(modelId_prefix) {
if(versionInput.value === ''){
versionInput.style.border = '2px solid red';
btnFetchAnthropicModels.disabled = true;
- modelAnthropic.disabled = true;
+ toggleTomSelectDisabled(modelAnthropic, true);
modelAnthropic.selectedIndex = -1;
modelAnthropic.style.border = '';
}else{
versionInput.style.border = '';
btnFetchAnthropicModels.disabled = false;
- modelAnthropic.disabled = false;
+ toggleTomSelectDisabled(modelAnthropic, false);
if((modelAnthropic.selectedIndex === -1)||(modelAnthropic.value === '')){
modelAnthropic.style.border = '2px solid red';
}else{
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,