{ "version": 3, "sources": ["../../../node_modules/cash-dom/dist/cash.js", "../javascript/hamburger_menu.js", "../javascript/application.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/constants.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/helpers.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/defaults.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/dropdown.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/suggestions.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/persist.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/texts.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/templates.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/EventDispatcher.js", "../../../node_modules/@yaireo/tagify/dist/src/parts/events.js", "../../../node_modules/@yaireo/tagify/dist/src/tagify.js"], "sourcesContent": ["(function(){\n\"use strict\";\nvar doc = document;\nvar win = window;\nvar docEle = doc.documentElement;\nvar createElement = doc.createElement.bind(doc);\nvar div = createElement('div');\nvar table = createElement('table');\nvar tbody = createElement('tbody');\nvar tr = createElement('tr');\nvar isArray = Array.isArray, ArrayPrototype = Array.prototype;\nvar concat = ArrayPrototype.concat, filter = ArrayPrototype.filter, indexOf = ArrayPrototype.indexOf, map = ArrayPrototype.map, push = ArrayPrototype.push, slice = ArrayPrototype.slice, some = ArrayPrototype.some, splice = ArrayPrototype.splice;\nvar idRe = /^#(?:[\\w-]|\\\\.|[^\\x00-\\xa0])*$/;\nvar classRe = /^\\.(?:[\\w-]|\\\\.|[^\\x00-\\xa0])*$/;\nvar htmlRe = /<.+>/;\nvar tagRe = /^\\w+$/;\n// @require ./variables.ts\nfunction find(selector, context) {\n var isFragment = isDocumentFragment(context);\n return !selector || (!isFragment && !isDocument(context) && !isElement(context))\n ? []\n : !isFragment && classRe.test(selector)\n ? context.getElementsByClassName(selector.slice(1).replace(/\\\\/g, ''))\n : !isFragment && tagRe.test(selector)\n ? context.getElementsByTagName(selector)\n : context.querySelectorAll(selector);\n}\n// @require ./find.ts\n// @require ./variables.ts\nvar Cash = /** @class */ (function () {\n function Cash(selector, context) {\n if (!selector)\n return;\n if (isCash(selector))\n return selector;\n var eles = selector;\n if (isString(selector)) {\n var ctx = context || doc;\n eles = idRe.test(selector) && isDocument(ctx)\n ? ctx.getElementById(selector.slice(1).replace(/\\\\/g, ''))\n : htmlRe.test(selector)\n ? parseHTML(selector)\n : isCash(ctx)\n ? ctx.find(selector)\n : isString(ctx)\n ? cash(ctx).find(selector)\n : find(selector, ctx);\n if (!eles)\n return;\n }\n else if (isFunction(selector)) {\n return this.ready(selector); //FIXME: `fn.ready` is not included in `core`, but it's actually a core functionality\n }\n if (eles.nodeType || eles === win)\n eles = [eles];\n this.length = eles.length;\n for (var i = 0, l = this.length; i < l; i++) {\n this[i] = eles[i];\n }\n }\n Cash.prototype.init = function (selector, context) {\n return new Cash(selector, context);\n };\n return Cash;\n}());\nvar fn = Cash.prototype;\nvar cash = fn.init;\ncash.fn = cash.prototype = fn; // Ensuring that `cash () instanceof cash`\nfn.length = 0;\nfn.splice = splice; // Ensuring a cash collection gets printed as array-like in Chrome's devtools\nif (typeof Symbol === 'function') { // Ensuring a cash collection is iterable\n fn[Symbol['iterator']] = ArrayPrototype[Symbol['iterator']];\n}\nfunction isCash(value) {\n return value instanceof Cash;\n}\nfunction isWindow(value) {\n return !!value && value === value.window;\n}\nfunction isDocument(value) {\n return !!value && value.nodeType === 9;\n}\nfunction isDocumentFragment(value) {\n return !!value && value.nodeType === 11;\n}\nfunction isElement(value) {\n return !!value && value.nodeType === 1;\n}\nfunction isText(value) {\n return !!value && value.nodeType === 3;\n}\nfunction isBoolean(value) {\n return typeof value === 'boolean';\n}\nfunction isFunction(value) {\n return typeof value === 'function';\n}\nfunction isString(value) {\n return typeof value === 'string';\n}\nfunction isUndefined(value) {\n return value === undefined;\n}\nfunction isNull(value) {\n return value === null;\n}\nfunction isNumeric(value) {\n return !isNaN(parseFloat(value)) && isFinite(value);\n}\nfunction isPlainObject(value) {\n if (typeof value !== 'object' || value === null)\n return false;\n var proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n}\ncash.isWindow = isWindow;\ncash.isFunction = isFunction;\ncash.isArray = isArray;\ncash.isNumeric = isNumeric;\ncash.isPlainObject = isPlainObject;\nfunction each(arr, callback, _reverse) {\n if (_reverse) {\n var i = arr.length;\n while (i--) {\n if (callback.call(arr[i], i, arr[i]) === false)\n return arr;\n }\n }\n else if (isPlainObject(arr)) {\n var keys = Object.keys(arr);\n for (var i = 0, l = keys.length; i < l; i++) {\n var key = keys[i];\n if (callback.call(arr[key], key, arr[key]) === false)\n return arr;\n }\n }\n else {\n for (var i = 0, l = arr.length; i < l; i++) {\n if (callback.call(arr[i], i, arr[i]) === false)\n return arr;\n }\n }\n return arr;\n}\ncash.each = each;\nfn.each = function (callback) {\n return each(this, callback);\n};\nfn.empty = function () {\n return this.each(function (i, ele) {\n while (ele.firstChild) {\n ele.removeChild(ele.firstChild);\n }\n });\n};\nfunction extend() {\n var sources = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n sources[_i] = arguments[_i];\n }\n var deep = isBoolean(sources[0]) ? sources.shift() : false;\n var target = sources.shift();\n var length = sources.length;\n if (!target)\n return {};\n if (!length)\n return extend(deep, cash, target);\n for (var i = 0; i < length; i++) {\n var source = sources[i];\n for (var key in source) {\n if (deep && (isArray(source[key]) || isPlainObject(source[key]))) {\n if (!target[key] || target[key].constructor !== source[key].constructor)\n target[key] = new source[key].constructor();\n extend(deep, target[key], source[key]);\n }\n else {\n target[key] = source[key];\n }\n }\n }\n return target;\n}\ncash.extend = extend;\nfn.extend = function (plugins) {\n return extend(fn, plugins);\n};\n// @require ./type_checking.ts\nvar splitValuesRe = /\\S+/g;\nfunction getSplitValues(str) {\n return isString(str) ? str.match(splitValuesRe) || [] : [];\n}\nfn.toggleClass = function (cls, force) {\n var classes = getSplitValues(cls);\n var isForce = !isUndefined(force);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n each(classes, function (i, c) {\n if (isForce) {\n force ? ele.classList.add(c) : ele.classList.remove(c);\n }\n else {\n ele.classList.toggle(c);\n }\n });\n });\n};\nfn.addClass = function (cls) {\n return this.toggleClass(cls, true);\n};\nfn.removeAttr = function (attr) {\n var attrs = getSplitValues(attr);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n each(attrs, function (i, a) {\n ele.removeAttribute(a);\n });\n });\n};\nfunction attr(attr, value) {\n if (!attr)\n return;\n if (isString(attr)) {\n if (arguments.length < 2) {\n if (!this[0] || !isElement(this[0]))\n return;\n var value_1 = this[0].getAttribute(attr);\n return isNull(value_1) ? undefined : value_1;\n }\n if (isUndefined(value))\n return this;\n if (isNull(value))\n return this.removeAttr(attr);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n ele.setAttribute(attr, value);\n });\n }\n for (var key in attr) {\n this.attr(key, attr[key]);\n }\n return this;\n}\nfn.attr = attr;\nfn.removeClass = function (cls) {\n if (arguments.length)\n return this.toggleClass(cls, false);\n return this.attr('class', '');\n};\nfn.hasClass = function (cls) {\n return !!cls && some.call(this, function (ele) { return isElement(ele) && ele.classList.contains(cls); });\n};\nfn.get = function (index) {\n if (isUndefined(index))\n return slice.call(this);\n index = Number(index);\n return this[index < 0 ? index + this.length : index];\n};\nfn.eq = function (index) {\n return cash(this.get(index));\n};\nfn.first = function () {\n return this.eq(0);\n};\nfn.last = function () {\n return this.eq(-1);\n};\nfunction text(text) {\n if (isUndefined(text)) {\n return this.get().map(function (ele) { return isElement(ele) || isText(ele) ? ele.textContent : ''; }).join('');\n }\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n ele.textContent = text;\n });\n}\nfn.text = text;\n// @require core/type_checking.ts\n// @require core/variables.ts\nfunction computeStyle(ele, prop, isVariable) {\n if (!isElement(ele))\n return;\n var style = win.getComputedStyle(ele, null);\n return isVariable ? style.getPropertyValue(prop) || undefined : style[prop] || ele.style[prop];\n}\n// @require ./compute_style.ts\nfunction computeStyleInt(ele, prop) {\n return parseInt(computeStyle(ele, prop), 10) || 0;\n}\n// @require css/helpers/compute_style_int.ts\nfunction getExtraSpace(ele, xAxis) {\n return computeStyleInt(ele, \"border\".concat(xAxis ? 'Left' : 'Top', \"Width\")) + computeStyleInt(ele, \"padding\".concat(xAxis ? 'Left' : 'Top')) + computeStyleInt(ele, \"padding\".concat(xAxis ? 'Right' : 'Bottom')) + computeStyleInt(ele, \"border\".concat(xAxis ? 'Right' : 'Bottom', \"Width\"));\n}\n// @require css/helpers/compute_style.ts\nvar defaultDisplay = {};\nfunction getDefaultDisplay(tagName) {\n if (defaultDisplay[tagName])\n return defaultDisplay[tagName];\n var ele = createElement(tagName);\n doc.body.insertBefore(ele, null);\n var display = computeStyle(ele, 'display');\n doc.body.removeChild(ele);\n return defaultDisplay[tagName] = display !== 'none' ? display : 'block';\n}\n// @require css/helpers/compute_style.ts\nfunction isHidden(ele) {\n return computeStyle(ele, 'display') === 'none';\n}\n// @require ./cash.ts\nfunction matches(ele, selector) {\n var matches = ele && (ele['matches'] || ele['webkitMatchesSelector'] || ele['msMatchesSelector']);\n return !!matches && !!selector && matches.call(ele, selector);\n}\n// @require ./matches.ts\n// @require ./type_checking.ts\nfunction getCompareFunction(comparator) {\n return isString(comparator)\n ? function (i, ele) { return matches(ele, comparator); }\n : isFunction(comparator)\n ? comparator\n : isCash(comparator)\n ? function (i, ele) { return comparator.is(ele); }\n : !comparator\n ? function () { return false; }\n : function (i, ele) { return ele === comparator; };\n}\nfn.filter = function (comparator) {\n var compare = getCompareFunction(comparator);\n return cash(filter.call(this, function (ele, i) { return compare.call(ele, i, ele); }));\n};\n// @require collection/filter.ts\nfunction filtered(collection, comparator) {\n return !comparator ? collection : collection.filter(comparator);\n}\nfn.detach = function (comparator) {\n filtered(this, comparator).each(function (i, ele) {\n if (ele.parentNode) {\n ele.parentNode.removeChild(ele);\n }\n });\n return this;\n};\nvar fragmentRe = /^\\s*<(\\w+)[^>]*>/;\nvar singleTagRe = /^<(\\w+)\\s*\\/?>(?:<\\/\\1>)?$/;\nvar containers = {\n '*': div,\n tr: tbody,\n td: tr,\n th: tr,\n thead: table,\n tbody: table,\n tfoot: table\n};\n//TODO: Create elements inside a document fragment, in order to prevent inline event handlers from firing\n//TODO: Ensure the created elements have the fragment as their parent instead of null, this also ensures we can deal with detatched nodes more reliably\nfunction parseHTML(html) {\n if (!isString(html))\n return [];\n if (singleTagRe.test(html))\n return [createElement(RegExp.$1)];\n var fragment = fragmentRe.test(html) && RegExp.$1;\n var container = containers[fragment] || containers['*'];\n container.innerHTML = html;\n return cash(container.childNodes).detach().get();\n}\ncash.parseHTML = parseHTML;\nfn.has = function (selector) {\n var comparator = isString(selector)\n ? function (i, ele) { return find(selector, ele).length; }\n : function (i, ele) { return ele.contains(selector); };\n return this.filter(comparator);\n};\nfn.not = function (comparator) {\n var compare = getCompareFunction(comparator);\n return this.filter(function (i, ele) { return (!isString(comparator) || isElement(ele)) && !compare.call(ele, i, ele); });\n};\nfunction pluck(arr, prop, deep, until) {\n var plucked = [];\n var isCallback = isFunction(prop);\n var compare = until && getCompareFunction(until);\n for (var i = 0, l = arr.length; i < l; i++) {\n if (isCallback) {\n var val_1 = prop(arr[i]);\n if (val_1.length)\n push.apply(plucked, val_1);\n }\n else {\n var val_2 = arr[i][prop];\n while (val_2 != null) {\n if (until && compare(-1, val_2))\n break;\n plucked.push(val_2);\n val_2 = deep ? val_2[prop] : null;\n }\n }\n }\n return plucked;\n}\n// @require core/pluck.ts\n// @require core/variables.ts\nfunction getValue(ele) {\n if (ele.multiple && ele.options)\n return pluck(filter.call(ele.options, function (option) { return option.selected && !option.disabled && !option.parentNode.disabled; }), 'value');\n return ele.value || '';\n}\nfunction val(value) {\n if (!arguments.length)\n return this[0] && getValue(this[0]);\n return this.each(function (i, ele) {\n var isSelect = ele.multiple && ele.options;\n if (isSelect || checkableRe.test(ele.type)) {\n var eleValue_1 = isArray(value) ? map.call(value, String) : (isNull(value) ? [] : [String(value)]);\n if (isSelect) {\n each(ele.options, function (i, option) {\n option.selected = eleValue_1.indexOf(option.value) >= 0;\n }, true);\n }\n else {\n ele.checked = eleValue_1.indexOf(ele.value) >= 0;\n }\n }\n else {\n ele.value = isUndefined(value) || isNull(value) ? '' : value;\n }\n });\n}\nfn.val = val;\nfn.is = function (comparator) {\n var compare = getCompareFunction(comparator);\n return some.call(this, function (ele, i) { return compare.call(ele, i, ele); });\n};\ncash.guid = 1;\nfunction unique(arr) {\n return arr.length > 1 ? filter.call(arr, function (item, index, self) { return indexOf.call(self, item) === index; }) : arr;\n}\ncash.unique = unique;\nfn.add = function (selector, context) {\n return cash(unique(this.get().concat(cash(selector, context).get())));\n};\nfn.children = function (comparator) {\n return filtered(cash(unique(pluck(this, function (ele) { return ele.children; }))), comparator);\n};\nfn.parent = function (comparator) {\n return filtered(cash(unique(pluck(this, 'parentNode'))), comparator);\n};\nfn.index = function (selector) {\n var child = selector ? cash(selector)[0] : this[0];\n var collection = selector ? this : cash(child).parent().children();\n return indexOf.call(collection, child);\n};\nfn.closest = function (comparator) {\n var filtered = this.filter(comparator);\n if (filtered.length)\n return filtered;\n var $parent = this.parent();\n if (!$parent.length)\n return filtered;\n return $parent.closest(comparator);\n};\nfn.siblings = function (comparator) {\n return filtered(cash(unique(pluck(this, function (ele) { return cash(ele).parent().children().not(ele); }))), comparator);\n};\nfn.find = function (selector) {\n return cash(unique(pluck(this, function (ele) { return find(selector, ele); })));\n};\n// @require core/variables.ts\n// @require collection/filter.ts\n// @require traversal/find.ts\nvar HTMLCDATARe = /^\\s*\\s*$/g;\nvar scriptTypeRe = /^$|^module$|\\/(java|ecma)script/i;\nvar scriptAttributes = ['type', 'src', 'nonce', 'noModule'];\nfunction evalScripts(node, doc) {\n var collection = cash(node);\n collection.filter('script').add(collection.find('script')).each(function (i, ele) {\n if (scriptTypeRe.test(ele.type) && docEle.contains(ele)) { // The script type is supported // The element is attached to the DOM // Using `documentElement` for broader browser support\n var script_1 = createElement('script');\n script_1.text = ele.textContent.replace(HTMLCDATARe, '');\n each(scriptAttributes, function (i, attr) {\n if (ele[attr])\n script_1[attr] = ele[attr];\n });\n doc.head.insertBefore(script_1, null);\n doc.head.removeChild(script_1);\n }\n });\n}\n// @require ./eval_scripts.ts\nfunction insertElement(anchor, target, left, inside, evaluate) {\n if (inside) { // prepend/append\n anchor.insertBefore(target, left ? anchor.firstChild : null);\n }\n else { // before/after\n if (anchor.nodeName === 'HTML') {\n anchor.parentNode.replaceChild(target, anchor);\n }\n else {\n anchor.parentNode.insertBefore(target, left ? anchor : anchor.nextSibling);\n }\n }\n if (evaluate) {\n evalScripts(target, anchor.ownerDocument);\n }\n}\n// @require ./insert_element.ts\nfunction insertSelectors(selectors, anchors, inverse, left, inside, reverseLoop1, reverseLoop2, reverseLoop3) {\n each(selectors, function (si, selector) {\n each(cash(selector), function (ti, target) {\n each(cash(anchors), function (ai, anchor) {\n var anchorFinal = inverse ? target : anchor;\n var targetFinal = inverse ? anchor : target;\n var indexFinal = inverse ? ti : ai;\n insertElement(anchorFinal, !indexFinal ? targetFinal : targetFinal.cloneNode(true), left, inside, !indexFinal);\n }, reverseLoop3);\n }, reverseLoop2);\n }, reverseLoop1);\n return anchors;\n}\nfn.after = function () {\n return insertSelectors(arguments, this, false, false, false, true, true);\n};\nfn.append = function () {\n return insertSelectors(arguments, this, false, false, true);\n};\nfunction html(html) {\n if (!arguments.length)\n return this[0] && this[0].innerHTML;\n if (isUndefined(html))\n return this;\n var hasScript = /]/.test(html);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n if (hasScript) {\n cash(ele).empty().append(html);\n }\n else {\n ele.innerHTML = html;\n }\n });\n}\nfn.html = html;\nfn.appendTo = function (selector) {\n return insertSelectors(arguments, this, true, false, true);\n};\nfn.wrapInner = function (selector) {\n return this.each(function (i, ele) {\n var $ele = cash(ele);\n var contents = $ele.contents();\n contents.length ? contents.wrapAll(selector) : $ele.append(selector);\n });\n};\nfn.before = function () {\n return insertSelectors(arguments, this, false, true);\n};\nfn.wrapAll = function (selector) {\n var structure = cash(selector);\n var wrapper = structure[0];\n while (wrapper.children.length)\n wrapper = wrapper.firstElementChild;\n this.first().before(structure);\n return this.appendTo(wrapper);\n};\nfn.wrap = function (selector) {\n return this.each(function (i, ele) {\n var wrapper = cash(selector)[0];\n cash(ele).wrapAll(!i ? wrapper : wrapper.cloneNode(true));\n });\n};\nfn.insertAfter = function (selector) {\n return insertSelectors(arguments, this, true, false, false, false, false, true);\n};\nfn.insertBefore = function (selector) {\n return insertSelectors(arguments, this, true, true);\n};\nfn.prepend = function () {\n return insertSelectors(arguments, this, false, true, true, true, true);\n};\nfn.prependTo = function (selector) {\n return insertSelectors(arguments, this, true, true, true, false, false, true);\n};\nfn.contents = function () {\n return cash(unique(pluck(this, function (ele) { return ele.tagName === 'IFRAME' ? [ele.contentDocument] : (ele.tagName === 'TEMPLATE' ? ele.content.childNodes : ele.childNodes); })));\n};\nfn.next = function (comparator, _all, _until) {\n return filtered(cash(unique(pluck(this, 'nextElementSibling', _all, _until))), comparator);\n};\nfn.nextAll = function (comparator) {\n return this.next(comparator, true);\n};\nfn.nextUntil = function (until, comparator) {\n return this.next(comparator, true, until);\n};\nfn.parents = function (comparator, _until) {\n return filtered(cash(unique(pluck(this, 'parentElement', true, _until))), comparator);\n};\nfn.parentsUntil = function (until, comparator) {\n return this.parents(comparator, until);\n};\nfn.prev = function (comparator, _all, _until) {\n return filtered(cash(unique(pluck(this, 'previousElementSibling', _all, _until))), comparator);\n};\nfn.prevAll = function (comparator) {\n return this.prev(comparator, true);\n};\nfn.prevUntil = function (until, comparator) {\n return this.prev(comparator, true, until);\n};\nfn.map = function (callback) {\n return cash(concat.apply([], map.call(this, function (ele, i) { return callback.call(ele, i, ele); })));\n};\nfn.clone = function () {\n return this.map(function (i, ele) { return ele.cloneNode(true); });\n};\nfn.offsetParent = function () {\n return this.map(function (i, ele) {\n var offsetParent = ele.offsetParent;\n while (offsetParent && computeStyle(offsetParent, 'position') === 'static') {\n offsetParent = offsetParent.offsetParent;\n }\n return offsetParent || docEle;\n });\n};\nfn.slice = function (start, end) {\n return cash(slice.call(this, start, end));\n};\n// @require ./cash.ts\nvar dashAlphaRe = /-([a-z])/g;\nfunction camelCase(str) {\n return str.replace(dashAlphaRe, function (match, letter) { return letter.toUpperCase(); });\n}\nfn.ready = function (callback) {\n var cb = function () { return setTimeout(callback, 0, cash); };\n if (doc.readyState !== 'loading') {\n cb();\n }\n else {\n doc.addEventListener('DOMContentLoaded', cb);\n }\n return this;\n};\nfn.unwrap = function () {\n this.parent().each(function (i, ele) {\n if (ele.tagName === 'BODY')\n return;\n var $ele = cash(ele);\n $ele.replaceWith($ele.children());\n });\n return this;\n};\nfn.offset = function () {\n var ele = this[0];\n if (!ele)\n return;\n var rect = ele.getBoundingClientRect();\n return {\n top: rect.top + win.pageYOffset,\n left: rect.left + win.pageXOffset\n };\n};\nfn.position = function () {\n var ele = this[0];\n if (!ele)\n return;\n var isFixed = (computeStyle(ele, 'position') === 'fixed');\n var offset = isFixed ? ele.getBoundingClientRect() : this.offset();\n if (!isFixed) {\n var doc_1 = ele.ownerDocument;\n var offsetParent = ele.offsetParent || doc_1.documentElement;\n while ((offsetParent === doc_1.body || offsetParent === doc_1.documentElement) && computeStyle(offsetParent, 'position') === 'static') {\n offsetParent = offsetParent.parentNode;\n }\n if (offsetParent !== ele && isElement(offsetParent)) {\n var parentOffset = cash(offsetParent).offset();\n offset.top -= parentOffset.top + computeStyleInt(offsetParent, 'borderTopWidth');\n offset.left -= parentOffset.left + computeStyleInt(offsetParent, 'borderLeftWidth');\n }\n }\n return {\n top: offset.top - computeStyleInt(ele, 'marginTop'),\n left: offset.left - computeStyleInt(ele, 'marginLeft')\n };\n};\nvar propMap = {\n /* GENERAL */\n class: 'className',\n contenteditable: 'contentEditable',\n /* LABEL */\n for: 'htmlFor',\n /* INPUT */\n readonly: 'readOnly',\n maxlength: 'maxLength',\n tabindex: 'tabIndex',\n /* TABLE */\n colspan: 'colSpan',\n rowspan: 'rowSpan',\n /* IMAGE */\n usemap: 'useMap'\n};\nfn.prop = function (prop, value) {\n if (!prop)\n return;\n if (isString(prop)) {\n prop = propMap[prop] || prop;\n if (arguments.length < 2)\n return this[0] && this[0][prop];\n return this.each(function (i, ele) { ele[prop] = value; });\n }\n for (var key in prop) {\n this.prop(key, prop[key]);\n }\n return this;\n};\nfn.removeProp = function (prop) {\n return this.each(function (i, ele) { delete ele[propMap[prop] || prop]; });\n};\nvar cssVariableRe = /^--/;\n// @require ./variables.ts\nfunction isCSSVariable(prop) {\n return cssVariableRe.test(prop);\n}\n// @require core/camel_case.ts\n// @require core/cash.ts\n// @require core/each.ts\n// @require core/variables.ts\n// @require ./is_css_variable.ts\nvar prefixedProps = {};\nvar style = div.style;\nvar vendorsPrefixes = ['webkit', 'moz', 'ms'];\nfunction getPrefixedProp(prop, isVariable) {\n if (isVariable === void 0) { isVariable = isCSSVariable(prop); }\n if (isVariable)\n return prop;\n if (!prefixedProps[prop]) {\n var propCC = camelCase(prop);\n var propUC = \"\".concat(propCC[0].toUpperCase()).concat(propCC.slice(1));\n var props = (\"\".concat(propCC, \" \").concat(vendorsPrefixes.join(\"\".concat(propUC, \" \"))).concat(propUC)).split(' ');\n each(props, function (i, p) {\n if (p in style) {\n prefixedProps[prop] = p;\n return false;\n }\n });\n }\n return prefixedProps[prop];\n}\n// @require core/type_checking.ts\n// @require ./is_css_variable.ts\nvar numericProps = {\n animationIterationCount: true,\n columnCount: true,\n flexGrow: true,\n flexShrink: true,\n fontWeight: true,\n gridArea: true,\n gridColumn: true,\n gridColumnEnd: true,\n gridColumnStart: true,\n gridRow: true,\n gridRowEnd: true,\n gridRowStart: true,\n lineHeight: true,\n opacity: true,\n order: true,\n orphans: true,\n widows: true,\n zIndex: true\n};\nfunction getSuffixedValue(prop, value, isVariable) {\n if (isVariable === void 0) { isVariable = isCSSVariable(prop); }\n return !isVariable && !numericProps[prop] && isNumeric(value) ? \"\".concat(value, \"px\") : value;\n}\nfunction css(prop, value) {\n if (isString(prop)) {\n var isVariable_1 = isCSSVariable(prop);\n prop = getPrefixedProp(prop, isVariable_1);\n if (arguments.length < 2)\n return this[0] && computeStyle(this[0], prop, isVariable_1);\n if (!prop)\n return this;\n value = getSuffixedValue(prop, value, isVariable_1);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n if (isVariable_1) {\n ele.style.setProperty(prop, value);\n }\n else {\n ele.style[prop] = value;\n }\n });\n }\n for (var key in prop) {\n this.css(key, prop[key]);\n }\n return this;\n}\n;\nfn.css = css;\nfunction attempt(fn, arg) {\n try {\n return fn(arg);\n }\n catch (_a) {\n return arg;\n }\n}\n// @require core/attempt.ts\n// @require core/camel_case.ts\nvar JSONStringRe = /^\\s+|\\s+$/;\nfunction getData(ele, key) {\n var value = ele.dataset[key] || ele.dataset[camelCase(key)];\n if (JSONStringRe.test(value))\n return value;\n return attempt(JSON.parse, value);\n}\n// @require core/attempt.ts\n// @require core/camel_case.ts\nfunction setData(ele, key, value) {\n value = attempt(JSON.stringify, value);\n ele.dataset[camelCase(key)] = value;\n}\nfunction data(name, value) {\n if (!name) {\n if (!this[0])\n return;\n var datas = {};\n for (var key in this[0].dataset) {\n datas[key] = getData(this[0], key);\n }\n return datas;\n }\n if (isString(name)) {\n if (arguments.length < 2)\n return this[0] && getData(this[0], name);\n if (isUndefined(value))\n return this;\n return this.each(function (i, ele) { setData(ele, name, value); });\n }\n for (var key in name) {\n this.data(key, name[key]);\n }\n return this;\n}\nfn.data = data;\nfunction getDocumentDimension(doc, dimension) {\n var docEle = doc.documentElement;\n return Math.max(doc.body[\"scroll\".concat(dimension)], docEle[\"scroll\".concat(dimension)], doc.body[\"offset\".concat(dimension)], docEle[\"offset\".concat(dimension)], docEle[\"client\".concat(dimension)]);\n}\neach([true, false], function (i, outer) {\n each(['Width', 'Height'], function (i, prop) {\n var name = \"\".concat(outer ? 'outer' : 'inner').concat(prop);\n fn[name] = function (includeMargins) {\n if (!this[0])\n return;\n if (isWindow(this[0]))\n return outer ? this[0][\"inner\".concat(prop)] : this[0].document.documentElement[\"client\".concat(prop)];\n if (isDocument(this[0]))\n return getDocumentDimension(this[0], prop);\n return this[0][\"\".concat(outer ? 'offset' : 'client').concat(prop)] + (includeMargins && outer ? computeStyleInt(this[0], \"margin\".concat(i ? 'Top' : 'Left')) + computeStyleInt(this[0], \"margin\".concat(i ? 'Bottom' : 'Right')) : 0);\n };\n });\n});\neach(['Width', 'Height'], function (index, prop) {\n var propLC = prop.toLowerCase();\n fn[propLC] = function (value) {\n if (!this[0])\n return isUndefined(value) ? undefined : this;\n if (!arguments.length) {\n if (isWindow(this[0]))\n return this[0].document.documentElement[\"client\".concat(prop)];\n if (isDocument(this[0]))\n return getDocumentDimension(this[0], prop);\n return this[0].getBoundingClientRect()[propLC] - getExtraSpace(this[0], !index);\n }\n var valueNumber = parseInt(value, 10);\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n var boxSizing = computeStyle(ele, 'boxSizing');\n ele.style[propLC] = getSuffixedValue(propLC, valueNumber + (boxSizing === 'border-box' ? getExtraSpace(ele, !index) : 0));\n });\n };\n});\nvar displayProperty = '___cd';\nfn.toggle = function (force) {\n return this.each(function (i, ele) {\n if (!isElement(ele))\n return;\n var hidden = isHidden(ele);\n var show = isUndefined(force) ? hidden : force;\n if (show) {\n ele.style.display = ele[displayProperty] || '';\n if (isHidden(ele)) {\n ele.style.display = getDefaultDisplay(ele.tagName);\n }\n }\n else if (!hidden) {\n ele[displayProperty] = computeStyle(ele, 'display');\n ele.style.display = 'none';\n }\n });\n};\nfn.hide = function () {\n return this.toggle(false);\n};\nfn.show = function () {\n return this.toggle(true);\n};\nvar eventsNamespace = '___ce';\nvar eventsNamespacesSeparator = '.';\nvar eventsFocus = { focus: 'focusin', blur: 'focusout' };\nvar eventsHover = { mouseenter: 'mouseover', mouseleave: 'mouseout' };\nvar eventsMouseRe = /^(mouse|pointer|contextmenu|drag|drop|click|dblclick)/i;\n// @require ./variables.ts\nfunction getEventNameBubbling(name) {\n return eventsHover[name] || eventsFocus[name] || name;\n}\n// @require ./variables.ts\nfunction parseEventName(eventName) {\n var parts = eventName.split(eventsNamespacesSeparator);\n return [parts[0], parts.slice(1).sort()]; // [name, namespace[]]\n}\nfn.trigger = function (event, data) {\n if (isString(event)) {\n var _a = parseEventName(event), nameOriginal = _a[0], namespaces = _a[1];\n var name_1 = getEventNameBubbling(nameOriginal);\n if (!name_1)\n return this;\n var type = eventsMouseRe.test(name_1) ? 'MouseEvents' : 'HTMLEvents';\n event = doc.createEvent(type);\n event.initEvent(name_1, true, true);\n event.namespace = namespaces.join(eventsNamespacesSeparator);\n event.___ot = nameOriginal;\n }\n event.___td = data;\n var isEventFocus = (event.___ot in eventsFocus);\n return this.each(function (i, ele) {\n if (isEventFocus && isFunction(ele[event.___ot])) {\n ele[\"___i\".concat(event.type)] = true; // Ensuring the native event is ignored\n ele[event.___ot]();\n ele[\"___i\".concat(event.type)] = false; // Ensuring the custom event is not ignored\n }\n ele.dispatchEvent(event);\n });\n};\n// @require ./variables.ts\nfunction getEventsCache(ele) {\n return ele[eventsNamespace] = (ele[eventsNamespace] || {});\n}\n// @require core/guid.ts\n// @require events/helpers/get_events_cache.ts\nfunction addEvent(ele, name, namespaces, selector, callback) {\n var eventCache = getEventsCache(ele);\n eventCache[name] = (eventCache[name] || []);\n eventCache[name].push([namespaces, selector, callback]);\n ele.addEventListener(name, callback);\n}\nfunction hasNamespaces(ns1, ns2) {\n return !ns2 || !some.call(ns2, function (ns) { return ns1.indexOf(ns) < 0; });\n}\n// @require ./get_events_cache.ts\n// @require ./has_namespaces.ts\n// @require ./parse_event_name.ts\nfunction removeEvent(ele, name, namespaces, selector, callback) {\n var cache = getEventsCache(ele);\n if (!name) {\n for (name in cache) {\n removeEvent(ele, name, namespaces, selector, callback);\n }\n }\n else if (cache[name]) {\n cache[name] = cache[name].filter(function (_a) {\n var ns = _a[0], sel = _a[1], cb = _a[2];\n if ((callback && cb.guid !== callback.guid) || !hasNamespaces(ns, namespaces) || (selector && selector !== sel))\n return true;\n ele.removeEventListener(name, cb);\n });\n }\n}\nfn.off = function (eventFullName, selector, callback) {\n var _this = this;\n if (isUndefined(eventFullName)) {\n this.each(function (i, ele) {\n if (!isElement(ele) && !isDocument(ele) && !isWindow(ele))\n return;\n removeEvent(ele);\n });\n }\n else if (!isString(eventFullName)) {\n for (var key in eventFullName) {\n this.off(key, eventFullName[key]);\n }\n }\n else {\n if (isFunction(selector)) {\n callback = selector;\n selector = '';\n }\n each(getSplitValues(eventFullName), function (i, eventFullName) {\n var _a = parseEventName(eventFullName), nameOriginal = _a[0], namespaces = _a[1];\n var name = getEventNameBubbling(nameOriginal);\n _this.each(function (i, ele) {\n if (!isElement(ele) && !isDocument(ele) && !isWindow(ele))\n return;\n removeEvent(ele, name, namespaces, selector, callback);\n });\n });\n }\n return this;\n};\nfn.remove = function (comparator) {\n filtered(this, comparator).detach().off();\n return this;\n};\nfn.replaceWith = function (selector) {\n return this.before(selector).remove();\n};\nfn.replaceAll = function (selector) {\n cash(selector).replaceWith(this);\n return this;\n};\nfunction on(eventFullName, selector, data, callback, _one) {\n var _this = this;\n if (!isString(eventFullName)) {\n for (var key in eventFullName) {\n this.on(key, selector, data, eventFullName[key], _one);\n }\n return this;\n }\n if (!isString(selector)) {\n if (isUndefined(selector) || isNull(selector)) {\n selector = '';\n }\n else if (isUndefined(data)) {\n data = selector;\n selector = '';\n }\n else {\n callback = data;\n data = selector;\n selector = '';\n }\n }\n if (!isFunction(callback)) {\n callback = data;\n data = undefined;\n }\n if (!callback)\n return this;\n each(getSplitValues(eventFullName), function (i, eventFullName) {\n var _a = parseEventName(eventFullName), nameOriginal = _a[0], namespaces = _a[1];\n var name = getEventNameBubbling(nameOriginal);\n var isEventHover = (nameOriginal in eventsHover);\n var isEventFocus = (nameOriginal in eventsFocus);\n if (!name)\n return;\n _this.each(function (i, ele) {\n if (!isElement(ele) && !isDocument(ele) && !isWindow(ele))\n return;\n var finalCallback = function (event) {\n if (event.target[\"___i\".concat(event.type)])\n return event.stopImmediatePropagation(); // Ignoring native event in favor of the upcoming custom one\n if (event.namespace && !hasNamespaces(namespaces, event.namespace.split(eventsNamespacesSeparator)))\n return;\n if (!selector && ((isEventFocus && (event.target !== ele || event.___ot === name)) || (isEventHover && event.relatedTarget && ele.contains(event.relatedTarget))))\n return;\n var thisArg = ele;\n if (selector) {\n var target = event.target;\n while (!matches(target, selector)) {\n if (target === ele)\n return;\n target = target.parentNode;\n if (!target)\n return;\n }\n thisArg = target;\n }\n Object.defineProperty(event, 'currentTarget', {\n configurable: true,\n get: function () {\n return thisArg;\n }\n });\n Object.defineProperty(event, 'delegateTarget', {\n configurable: true,\n get: function () {\n return ele;\n }\n });\n Object.defineProperty(event, 'data', {\n configurable: true,\n get: function () {\n return data;\n }\n });\n var returnValue = callback.call(thisArg, event, event.___td);\n if (_one) {\n removeEvent(ele, name, namespaces, selector, finalCallback);\n }\n if (returnValue === false) {\n event.preventDefault();\n event.stopPropagation();\n }\n };\n finalCallback.guid = callback.guid = (callback.guid || cash.guid++);\n addEvent(ele, name, namespaces, selector, finalCallback);\n });\n });\n return this;\n}\nfn.on = on;\nfunction one(eventFullName, selector, data, callback) {\n return this.on(eventFullName, selector, data, callback, true);\n}\n;\nfn.one = one;\nvar queryEncodeCRLFRe = /\\r?\\n/g;\nfunction queryEncode(prop, value) {\n return \"&\".concat(encodeURIComponent(prop), \"=\").concat(encodeURIComponent(value.replace(queryEncodeCRLFRe, '\\r\\n')));\n}\nvar skippableRe = /file|reset|submit|button|image/i;\nvar checkableRe = /radio|checkbox/i;\nfn.serialize = function () {\n var query = '';\n this.each(function (i, ele) {\n each(ele.elements || [ele], function (i, ele) {\n if (ele.disabled || !ele.name || ele.tagName === 'FIELDSET' || skippableRe.test(ele.type) || (checkableRe.test(ele.type) && !ele.checked))\n return;\n var value = getValue(ele);\n if (!isUndefined(value)) {\n var values = isArray(value) ? value : [value];\n each(values, function (i, value) {\n query += queryEncode(ele.name, value);\n });\n }\n });\n });\n return query.slice(1);\n};\n// @require core/types.ts\n// @require core/cash.ts\n// @require core/type_checking.ts\n// @require core/variables.ts\n// @require core/each.ts\n// @require core/extend.ts\n// @require core/find.ts\n// @require core/get_compare_function.ts\n// @require core/get_split_values.ts\n// @require core/guid.ts\n// @require core/parse_html.ts\n// @require core/unique.ts\n// @require attributes/add_class.ts\n// @require attributes/attr.ts\n// @require attributes/has_class.ts\n// @require attributes/prop.ts\n// @require attributes/remove_attr.ts\n// @require attributes/remove_class.ts\n// @require attributes/remove_prop.ts\n// @require attributes/toggle_class.ts\n// @require collection/add.ts\n// @require collection/each.ts\n// @require collection/eq.ts\n// @require collection/filter.ts\n// @require collection/first.ts\n// @require collection/get.ts\n// @require collection/index.ts\n// @require collection/last.ts\n// @require collection/map.ts\n// @require collection/slice.ts\n// @require css/css.ts\n// @require data/data.ts\n// @require dimensions/inner_outer.ts\n// @require dimensions/normal.ts\n// @require effects/hide.ts\n// @require effects/show.ts\n// @require effects/toggle.ts\n// @require events/off.ts\n// @require events/on.ts\n// @require events/one.ts\n// @require events/ready.ts\n// @require events/trigger.ts\n// @require forms/serialize.ts\n// @require forms/val.ts\n// @require manipulation/after.ts\n// @require manipulation/append.ts\n// @require manipulation/append_to.ts\n// @require manipulation/before.ts\n// @require manipulation/clone.ts\n// @require manipulation/detach.ts\n// @require manipulation/empty.ts\n// @require manipulation/html.ts\n// @require manipulation/insert_after.ts\n// @require manipulation/insert_before.ts\n// @require manipulation/prepend.ts\n// @require manipulation/prepend_to.ts\n// @require manipulation/remove.ts\n// @require manipulation/replace_all.ts\n// @require manipulation/replace_with.ts\n// @require manipulation/text.ts\n// @require manipulation/unwrap.ts\n// @require manipulation/wrap.ts\n// @require manipulation/wrap_all.ts\n// @require manipulation/wrap_inner.ts\n// @require offset/offset.ts\n// @require offset/offset_parent.ts\n// @require offset/position.ts\n// @require traversal/children.ts\n// @require traversal/closest.ts\n// @require traversal/contents.ts\n// @require traversal/find.ts\n// @require traversal/has.ts\n// @require traversal/is.ts\n// @require traversal/next.ts\n// @require traversal/next_all.ts\n// @require traversal/next_until.ts\n// @require traversal/not.ts\n// @require traversal/parent.ts\n// @require traversal/parents.ts\n// @require traversal/parents_until.ts\n// @require traversal/prev.ts\n// @require traversal/prev_all.ts\n// @require traversal/prev_until.ts\n// @require traversal/siblings.ts\n// @no-require extras/get_script.ts\n// @no-require extras/shorthands.ts\n// @require methods.ts\nif (typeof exports !== 'undefined') { // Node.js\n module.exports = cash;\n}\nelse { // Browser\n win['cash'] = win['$'] = cash;\n}\n})();", "import $ from \"cash-dom\"\n\n// Hamburger menu for mobile\nwindow.addEventListener('load', () => {\n var visible = 0\n\n // Hamburger\n $('nav').on('click', () => {\n if($(window).width() < 730){\n visible = 1 - visible;\n $('nav ul').toggle(visible)\n }\n })\n\n $('html').on('click', () => {\n closeDropdowns()\n })\n $('li.dropdown').on('click', function(e){ \n e.stopPropagation()\n })\n\n $('li.dropdown button').on('click', function(e){\n let expand = ($(this).attr('aria-expanded') == 'false')\n\n // Close all\n closeDropdowns()\n\n // Open current if not already open\n if(expand){\n $(this).attr('aria-expanded', 'true')\n $(this).siblings('ul').show()\n }\n })\n\n function closeDropdowns() {\n $('li.dropdown button').attr('aria-expanded', 'false')\n $('li.dropdown ul').hide()\n }\n})\n", "// Entry point for the build script in your package.json\n\nrequire('./hamburger_menu')\n\nimport $ from 'cash-dom'\nwindow.$ = $\n\nimport Tagify from '@yaireo/tagify'\nwindow.Tagify = Tagify\n", "export var ZERO_WIDTH_CHAR = '\\u200B';\nexport var ZERO_WIDTH_UNICODE_CHAR = `​`\nexport const UPDATE_DELAY = 100\n", "import {ZERO_WIDTH_CHAR} from './constants'\n\nexport const logger = {\n isEnabled() { return window.TAGIFY_DEBUG ?? true},\n log(...args){ this.isEnabled() && console.log('[Tagify]:', ...args) },\n warn(...args) { this.isEnabled() && console.warn('[Tagify]:', ...args) }\n}\n\n// console.json = console.json || function(argument){\n// for(var arg=0; arg < arguments.length; ++arg)\n// console.log( JSON.stringify(arguments[arg], null, 4) )\n// }\n\n// const isEdge = /Edge/.test(navigator.userAgent)\nexport const sameStr = (s1, s2, caseSensitive, trim) => {\n // cast to String\n s1 = \"\"+s1;\n s2 = \"\"+s2;\n\n if( trim ){\n s1 = s1.trim()\n s2 = s2.trim()\n }\n\n return caseSensitive\n ? s1 == s2\n : s1.toLowerCase() == s2.toLowerCase()\n}\n\n\n// const getUID = () => (new Date().getTime() + Math.floor((Math.random()*10000)+1)).toString(16)\nexport const removeCollectionProp = (collection, unwantedProps) => collection && Array.isArray(collection) && collection.map(v => omit(v, unwantedProps))\n\nexport function omit(obj, props){\n var newObj = {}, p;\n for( p in obj )\n if( props.indexOf(p) < 0 )\n newObj[p] = obj[p]\n return newObj\n}\n\nexport function decode( s ) {\n var el = document.createElement('div');\n return s.replace(/\\&#?[0-9a-z]+;/gi, function(enc){\n el.innerHTML = enc;\n return el.innerText\n })\n}\n\n/**\n * utility method\n * https://stackoverflow.com/a/35385518/104380\n * @param {String} s [HTML string]\n * @return {Object} [DOM node]\n */\nexport function parseHTML( s ){\n var parser = new DOMParser(),\n node = parser.parseFromString(s.trim(), \"text/html\");\n\n return node.body.firstElementChild;\n}\n\n/**\n * Removed new lines and irrelevant spaces which might affect layout, and are better gone\n * @param {string} s [HTML string]\n */\nexport function minify( s ){\n return s ? s\n .replace(/\\>[\\r\\n ]+\\<\")\n .split(/>\\s+<').trim()\n : \"\"\n}\n\nexport function removeTextChildNodes( elm ){\n var iter = document.createNodeIterator(elm, NodeFilter.SHOW_TEXT, null, false),\n textnode;\n\n // print all text nodes\n while (textnode = iter.nextNode()){\n if( !textnode.textContent.trim() )\n textnode.parentNode.removeChild(textnode)\n }\n}\n\nexport function getfirstTextNode( elm, action ){\n action = action || 'previous';\n while ( elm = elm[action + 'Sibling'] )\n if( elm.nodeType == 3 )\n return elm\n}\n\n/**\n * utility method\n * https://stackoverflow.com/a/6234804/104380\n */\nexport function escapeHTML( s ){\n return typeof s == 'string' ? s\n .replace(/&/g, \"&\")\n .replace(//g, \">\")\n .replace(/\"/g, \""\")\n .replace(/`|'/g, \"'\")\n : s;\n}\n\n/**\n * Checks if an argument is a javascript Object\n */\nexport function isObject(obj) {\n var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1);\n return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement';\n}\n\n/**\n * merge objects into a single new one\n * TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})\n */\nexport function extend( o, o1, o2) {\n if( !(o instanceof Object) ) o = {};\n\n copy(o, o1);\n if( o2 )\n copy(o, o2)\n\n function copy(a,b){\n // copy o2 to o\n for( var key in b )\n if( b.hasOwnProperty(key) ){\n if( isObject(b[key]) ){\n if( !isObject(a[key]) )\n a[key] = Object.assign({}, b[key])\n else\n copy(a[key], b[key])\n\n continue;\n }\n\n if( Array.isArray(b[key]) ){\n a[key] = Object.assign([], b[key])\n continue\n }\n\n a[key] = b[key]\n }\n }\n\n return o\n}\n\n/**\n * concatenates N arrays without dups.\n * If an array's item is an Object, compare by `value`\n */\nexport function concatWithoutDups(){\n const newArr = [],\n existingObj = {};\n\n for( let arr of arguments ) {\n for( let item of arr ) {\n // if current item is an object which has yet to be added to the new array\n if( isObject(item) ){\n if( !existingObj[item.value] ){\n newArr.push(item)\n existingObj[item.value] = 1\n }\n }\n\n // if current item is not an object and is not in the new array\n else if( !newArr.includes(item) )\n newArr.push(item)\n }\n }\n\n return newArr\n}\n\n/**\n * Extracted from: https://stackoverflow.com/a/37511463/104380\n * @param {String} s\n */\nexport function unaccent( s ){\n // if not supported, do not continue.\n // developers should use a polyfill:\n // https://github.com/walling/unorm\n if( !String.prototype.normalize )\n return s\n\n if (typeof(s) === 'string')\n return s.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\")\n}\n\n/**\n * Meassures an element's height, which might yet have been added DOM\n * https://stackoverflow.com/q/5944038/104380\n * @param {DOM} node\n */\nexport function getNodeHeight( node ){\n var height, clone = node.cloneNode(true)\n clone.style.cssText = \"position:fixed; top:-9999px; opacity:0\"\n document.body.appendChild(clone)\n height = clone.clientHeight\n clone.parentNode.removeChild(clone)\n return height\n}\n\nexport var isChromeAndroidBrowser = () => /(?=.*chrome)(?=.*android)/i.test(navigator.userAgent)\n\nexport function getUID() {\n return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>\n (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)\n )\n}\n\nexport function isNodeTag(node){\n const belongs = isNodeBelongsToThisTagifyInstance.call(this, node);\n const isTag = node?.classList?.contains(this.settings.classNames.tag)\n return belongs && isTag\n}\n\nexport function isWithinNodeTag(node){\n return isNodeBelongsToThisTagifyInstance.call(this, node) && node?.closest(this.settings.classNames.tagSelector)\n}\n\nfunction isNodeBelongsToThisTagifyInstance(node) {\n let closestTagifyNode = node?.closest?.(this.settings.classNames.namespaceSelector)\n return closestTagifyNode === this.DOM.scope\n}\n\n/**\n* Get the caret position relative to the viewport\n* https://stackoverflow.com/q/58985076/104380\n*\n* @returns {object} left, top distance in pixels\n*/\nexport function getCaretGlobalPosition(){\n const sel = document.getSelection()\n\n if( sel.rangeCount ){\n const r = sel.getRangeAt(0)\n const node = r.startContainer\n const offset = r.startOffset\n let rect, r2;\n\n if (offset > 0) {\n r2 = document.createRange()\n r2.setStart(node, offset - 1)\n r2.setEnd(node, offset)\n rect = r2.getBoundingClientRect()\n return {left:rect.right, top:rect.top, bottom:rect.bottom}\n }\n\n if( node.getBoundingClientRect )\n return node.getBoundingClientRect()\n }\n\n return {left:-9999, top:-9999}\n}\n\n/**\n * Injects content (either string or node) at the current the current (or specificed) caret position\n * @param {content} string/node\n * @param {range} Object (optional, a range other than the current window selection)\n */\nexport function injectAtCaret(content, range){\n var selection = window.getSelection();\n range = range || selection.getRangeAt(0)\n\n if( typeof content == 'string' )\n content = document.createTextNode(content)\n\n if( range ) {\n range.deleteContents()\n range.insertNode(content)\n }\n\n return content\n}\n\n/** Setter/Getter\n * Each tag DOM node contains a custom property called \"__tagifyTagData\" which hosts its data\n * @param {Node} tagElm\n * @param {Object} data\n */\nexport function getSetTagData(tagElm, data, override){\n if( !tagElm ){\n logger.warn(\"tag element doesn't exist\",{tagElm, data})\n return data\n }\n\n if( data )\n tagElm.__tagifyTagData = override\n ? data\n : extend({}, tagElm.__tagifyTagData || {}, data)\n\n return tagElm.__tagifyTagData\n}\n\nexport function placeCaretAfterNode( node ){\n if( !node || !node.parentNode ) return\n\n var nextSibling = node,\n sel = window.getSelection(),\n range = sel.getRangeAt(0);\n\n if (sel.rangeCount) {\n range.setStartAfter(nextSibling);\n range.collapse(true)\n // range.setEndBefore(nextSibling || node);\n sel.removeAllRanges();\n sel.addRange(range);\n }\n}\n\n/**\n * iterate all tags, checking if multiple ones are close-siblings and if so, add a zero-space width character between them,\n * which forces the caret to be rendered when the selection is between tags.\n * Also do that if the tag is the first node.\n * @param {Array} tags\n */\nexport function fixCaretBetweenTags(tags, TagifyHasFocuse) {\n tags.forEach(tag => {\n if( getSetTagData(tag.previousSibling) || !tag.previousSibling ) {\n var textNode = document.createTextNode(ZERO_WIDTH_CHAR)\n tag.before(textNode)\n TagifyHasFocuse && placeCaretAfterNode(textNode)\n }\n })\n}\n\n", "export default {\n delimiters : \",\", // [RegEx] split tags by any of these delimiters (\"null\" to cancel) Example: \",| |.\"\n pattern : null, // RegEx pattern to validate input by. Ex: /[1-9]/\n tagTextProp : 'value', // tag data Object property which will be displayed as the tag's text\n maxTags : Infinity, // Maximum number of tags\n callbacks : {}, // Exposed callbacks object to be triggered on certain events\n addTagOnBlur : true, // automatically adds the text which was inputed as a tag when blur event happens\n addTagOn : ['blur', 'tab', 'enter'], // if the tagify field (in a normal mode) has any non-tag input in it, convert it to a tag on any of these events: blur away from the field, click \"tab\"/\"enter\" key\n onChangeAfterBlur : true, // By default, the native way of inputs' onChange events is kept, and it only fires when the field is blured.\n duplicates : false, // \"true\" - allow duplicate tags\n whitelist : [], // Array of tags to suggest as the user types (can be used along with \"enforceWhitelist\" setting)\n blacklist : [], // A list of non-allowed tags\n enforceWhitelist : false, // Only allow tags from the whitelist\n userInput : true, // disable manually typing/pasting/editing tags (tags may only be added from the whitelist)\n focusable : true, // Allow the component as a whole to recieve focus. There are implementations of Tagify without external border and so 'focusability' causes unwanted behaviour\n focusInputOnRemove : true, // Refocus the input when a tag is removed\n keepInvalidTags : false, // if true, do not remove tags which did not pass validation\n createInvalidTags : true, // if false, do not create invalid tags from invalid user input\n mixTagsAllowedAfter : /,|\\.|\\:|\\s/, // RegEx - Define conditions in which mix-tags content allows a tag to be added after\n mixTagsInterpolator : ['[[', ']]'], // Interpolation for mix mode. Everything between these will become a tag, if is a valid Object\n backspace : true, // false / true / \"edit\"\n allowCaretBetweenTags : true, // if true, allows for caret between tags\n skipInvalid : false, // If `true`, do not add invalid, temporary, tags before automatically removing them\n pasteAsTags : true, // automatically converts pasted text into tags. if \"false\", allows for further text editing\n editTags : {\n clicks : 2, // clicks to enter \"edit-mode\": 1 for single click. any other value is considered as double-click\n keepInvalid : true // keeps invalid edits as-is until `esc` is pressed while in focus\n }, // 1 or 2 clicks to edit a tag. false/null for not allowing editing\n transformTag : ()=>{}, // Takes a tag input string as argument and returns a transformed value\n trim : true, // whether or not the value provided should be trimmed, before being added as a tag\n a11y: {\n focusableTags: false,\n inputAriaLabel: \"Tags input field\",\n },\n\n mixMode: {\n insertAfterTag : '\\u00A0', // String/Node to inject after a tag has been added (see #588)\n },\n\n autoComplete: {\n enabled: true, // Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text\n rightKey: false, // If `true`, when Right key is pressed, use the suggested value to create a tag, else just auto-completes the input. in mixed-mode this is set to \"true\"\n tabKey: false, // If 'true`, pressing `tab` key would only auto-complete but not also convert to a tag (like `rightKey` does).\n },\n\n classNames: {\n namespace : 'tagify',\n mixMode : 'tagify--mix',\n selectMode : 'tagify--select',\n input : 'tagify__input',\n focus : 'tagify--focus',\n tagNoAnimation : 'tagify--noAnim',\n tagInvalid : 'tagify--invalid',\n tagNotAllowed : 'tagify--notAllowed',\n scopeLoading : 'tagify--loading',\n hasMaxTags : 'tagify--hasMaxTags',\n hasNoTags : 'tagify--noTags',\n empty : 'tagify--empty',\n inputInvalid : 'tagify__input--invalid',\n dropdown : 'tagify__dropdown',\n dropdownWrapper : 'tagify__dropdown__wrapper',\n dropdownHeader : 'tagify__dropdown__header',\n dropdownFooter : 'tagify__dropdown__footer',\n dropdownItem : 'tagify__dropdown__item',\n dropdownItemActive : 'tagify__dropdown__item--active',\n dropdownItemHidden : 'tagify__dropdown__item--hidden',\n dropdownItemSelected : 'tagify__dropdown__item--selected',\n dropdownInital : 'tagify__dropdown--initial',\n tag : 'tagify__tag',\n tagText : 'tagify__tag-text',\n tagX : 'tagify__tag__removeBtn',\n tagLoading : 'tagify__tag--loading',\n tagEditing : 'tagify__tag--editable',\n tagFlash : 'tagify__tag--flash',\n tagHide : 'tagify__tag--hide',\n\n },\n\n dropdown: {\n classname : '',\n enabled : 2, // minimum input characters to be typed for the suggestions dropdown to show\n maxItems : 10,\n searchKeys : [\"value\", \"searchBy\"],\n fuzzySearch : true,\n caseSensitive : false,\n accentedSearch : true,\n includeSelectedTags: false, // Should the suggestions list Include already-selected tags (after filtering)\n escapeHTML : true, // escapes HTML entities in the suggestions' rendered text\n highlightFirst : true, // highlights first-matched item in the list\n closeOnSelect : true, // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown)\n clearOnSelect : true, // after selecting a suggetion, should the typed text input remain or be cleared\n position : 'all', // 'manual' / 'text' / 'all'\n appendTarget : null // defaults to document.body once DOM has been loaded\n },\n\n hooks: {\n beforeRemoveTag: () => Promise.resolve(),\n beforePaste: () => Promise.resolve(),\n suggestionClick: () => Promise.resolve(),\n beforeKeyDown: () => Promise.resolve(),\n }\n}\n", "import { sameStr, isObject, minify, getNodeHeight, getCaretGlobalPosition } from './helpers'\nimport suggestionsMethods from './suggestions'\n\nexport function initDropdown(){\n this.dropdown = {}\n\n // auto-bind \"this\" to all the dropdown methods\n for( let p in this._dropdown )\n this.dropdown[p] = typeof this._dropdown[p] === 'function'\n ? this._dropdown[p].bind(this)\n : this._dropdown[p]\n\n this.dropdown.refs()\n this.DOM.dropdown.__tagify = this\n}\n\nexport default {\n ...suggestionsMethods,\n\n refs(){\n this.DOM.dropdown = this.parseTemplate('dropdown', [this.settings])\n this.DOM.dropdown.content = this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-wrapper']\")\n },\n\n getHeaderRef(){\n return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-header']\")\n },\n\n getFooterRef(){\n return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-footer']\")\n },\n\n getAllSuggestionsRefs(){\n return [...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]\n },\n\n /**\n * shows the suggestions select box\n * @param {String} value [optional, filter the whitelist by this value]\n */\n show( value ){\n var _s = this.settings,\n firstListItem,\n firstListItemValue,\n allowNewTags = _s.mode == 'mix' && !_s.enforceWhitelist,\n noWhitelist = !_s.whitelist || !_s.whitelist.length,\n noMatchListItem,\n isManual = _s.dropdown.position == 'manual';\n\n // if text still exists in the input, and `show` method has no argument, then the input's text should be used\n value = value === undefined ? this.state.inputText : value\n\n // ⚠️ Do not render suggestions list if:\n // 1. there's no whitelist (can happen while async loading) AND new tags arn't allowed\n // 2. dropdown is disabled\n // 3. loader is showing (controlled outside of this code)\n if( (noWhitelist && !allowNewTags && !_s.templates.dropdownItemNoMatch)\n || _s.dropdown.enabled === false\n || this.state.isLoading\n || this.settings.readonly )\n return;\n\n clearTimeout(this.dropdownHide__bindEventsTimeout)\n\n // if no value was supplied, show all the \"whitelist\" items in the dropdown\n // @type [Array] listItems\n this.suggestedListItems = this.dropdown.filterListItems(value)\n\n // trigger at this exact point to let the developer the chance to manually set \"this.suggestedListItems\"\n if( value && !this.suggestedListItems.length ){\n this.trigger('dropdown:noMatch', value)\n\n if( _s.templates.dropdownItemNoMatch )\n noMatchListItem = _s.templates.dropdownItemNoMatch.call(this, {value})\n }\n\n // if \"dropdownItemNoMatch\" was not defined, procceed regular flow.\n //\n if( !noMatchListItem ){\n // in mix-mode, if the value isn't included in the whilelist & \"enforceWhitelist\" setting is \"false\",\n // then add a custom suggestion item to the dropdown\n if( this.suggestedListItems.length ){\n if( value && allowNewTags && !this.state.editing.scope && !sameStr(this.suggestedListItems[0].value, value) )\n this.suggestedListItems.unshift({value})\n }\n else{\n if( value && allowNewTags && !this.state.editing.scope ){\n this.suggestedListItems = [{value}]\n }\n // hide suggestions list if no suggestion matched\n else{\n this.input.autocomplete.suggest.call(this);\n this.dropdown.hide()\n return;\n }\n }\n\n firstListItem = this.suggestedListItems[0]\n firstListItemValue = \"\"+(isObject(firstListItem) ? firstListItem.value : firstListItem)\n\n if( _s.autoComplete && firstListItemValue ){\n // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of \"fuzzysearch\" setting)\n if( firstListItemValue.indexOf(value) == 0 )\n this.input.autocomplete.suggest.call(this, firstListItem)\n }\n }\n\n this.dropdown.fill(noMatchListItem)\n\n if( _s.dropdown.highlightFirst ) {\n this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(_s.classNames.dropdownItemSelector))\n }\n\n // bind events, exactly at this stage of the code. \"dropdown.show\" method is allowed to be\n // called multiple times, regardless if the dropdown is currently visible, but the events-binding\n // should only be called if the dropdown wasn't previously visible.\n if( !this.state.dropdown.visible )\n // timeout is needed for when pressing arrow down to show the dropdown,\n // so the key event won't get registered in the dropdown events listeners\n setTimeout(this.dropdown.events.binding.bind(this))\n\n // set the dropdown visible state to be the same as the searched value.\n // MUST be set *before* position() is called\n this.state.dropdown.visible = value || true\n this.state.dropdown.query = value\n\n this.setStateSelection()\n\n // try to positioning the dropdown (it might not yet be on the page, doesn't matter, next code handles this)\n if( !isManual ){\n // a slight delay is needed if the dropdown \"position\" setting is \"text\", and nothing was typed in the input,\n // so sadly the \"getCaretGlobalPosition\" method doesn't recognize the caret position without this delay\n setTimeout(() => {\n this.dropdown.position()\n this.dropdown.render()\n })\n }\n\n // a delay is needed because of the previous delay reason.\n // this event must be fired after the dropdown was rendered & positioned\n setTimeout(() => {\n this.trigger(\"dropdown:show\", this.DOM.dropdown)\n })\n },\n\n /**\n * Hides the dropdown (if it's not managed manually by the developer)\n * @param {Boolean} overrideManual\n */\n hide( overrideManual ){\n var {scope, dropdown} = this.DOM,\n isManual = this.settings.dropdown.position == 'manual' && !overrideManual;\n\n // if there's no dropdown, this means the dropdown events aren't binded\n if( !dropdown || !document.body.contains(dropdown) || isManual ) return;\n\n window.removeEventListener('resize', this.dropdown.position)\n this.dropdown.events.binding.call(this, false) // unbind all events\n\n // if the dropdown is open, and the input (scope) is clicked,\n // the dropdown should be now \"close\", and the next click (on the scope)\n // should re-open it, and without a timeout, clicking to close will re-open immediately\n // clearTimeout(this.dropdownHide__bindEventsTimeout)\n // this.dropdownHide__bindEventsTimeout = setTimeout(this.events.binding.bind(this), 250) // re-bind main events\n\n\n scope.setAttribute(\"aria-expanded\", false)\n dropdown.parentNode.removeChild(dropdown)\n\n // scenario: clicking the scope to show the dropdown, clicking again to hide -> calls dropdown.hide() and then re-focuses the input\n // which casues another onFocus event, which checked \"this.state.dropdown.visible\" and see it as \"false\" and re-open the dropdown\n setTimeout(() => {\n this.state.dropdown.visible = false\n }, 100)\n\n this.state.dropdown.query =\n this.state.ddItemData =\n this.state.ddItemElm =\n this.state.selection = null\n\n // if the user closed the dropdown (in mix-mode) while a potential tag was detected, flag the current tag\n // so the dropdown won't be shown on following user input for that \"tag\"\n if( this.state.tag && this.state.tag.value.length ){\n this.state.flaggedTags[this.state.tag.baseOffset] = this.state.tag\n }\n\n this.trigger(\"dropdown:hide\", dropdown)\n\n return this\n },\n\n /**\n * Toggles dropdown show/hide\n * @param {Boolean} show forces the dropdown to show\n */\n toggle(show){\n this.dropdown[this.state.dropdown.visible && !show ? 'hide' : 'show']()\n },\n\n getAppendTarget() {\n var _sd = this.settings.dropdown;\n return typeof _sd.appendTarget === 'function' ? _sd.appendTarget() : _sd.appendTarget;\n },\n\n render(){\n // let the element render in the DOM first, to accurately measure it.\n // this.DOM.dropdown.style.cssText = \"left:-9999px; top:-9999px;\";\n var ddHeight = getNodeHeight(this.DOM.dropdown),\n _s = this.settings,\n appendTarget = this.dropdown.getAppendTarget();\n\n if( _s.dropdown.enabled === false ) return this;\n\n this.DOM.scope.setAttribute(\"aria-expanded\", true)\n\n // if the dropdown has yet to be appended to the DOM,\n // append the dropdown to the body element & handle events\n if( !document.body.contains(this.DOM.dropdown) ){\n this.DOM.dropdown.classList.add( _s.classNames.dropdownInital )\n this.dropdown.position(ddHeight)\n appendTarget.appendChild(this.DOM.dropdown)\n\n setTimeout(() =>\n this.DOM.dropdown.classList.remove( _s.classNames.dropdownInital )\n )\n }\n\n return this\n },\n\n /**\n * re-renders the dropdown content element (see \"dropdownContent\" in templates file)\n * @param {String/Array} HTMLContent - optional\n */\n fill( HTMLContent ){\n HTMLContent = typeof HTMLContent == 'string'\n ? HTMLContent\n : this.dropdown.createListHTML(HTMLContent || this.suggestedListItems)\n\n var dropdownContent = this.settings.templates.dropdownContent.call(this, HTMLContent)\n\n this.DOM.dropdown.content.innerHTML = minify(dropdownContent)\n },\n\n /**\n * Re-renders only the header & footer.\n * Used when selecting a suggestion and it is wanted that the suggestions dropdown stays open.\n * Since the list of sugegstions is not being re-rendered completely every time a suggestion is selected (the item is transitioned-out)\n * then the header & footer should be kept in sync with the suggestions data change\n */\n fillHeaderFooter(){\n var suggestions = this.dropdown.filterListItems(this.state.dropdown.query),\n newHeaderElem = this.parseTemplate('dropdownHeader', [suggestions]),\n newFooterElem = this.parseTemplate('dropdownFooter', [suggestions]),\n headerRef = this.dropdown.getHeaderRef(),\n footerRef = this.dropdown.getFooterRef();\n\n newHeaderElem && headerRef?.parentNode.replaceChild(newHeaderElem, headerRef)\n newFooterElem && footerRef?.parentNode.replaceChild(newFooterElem, footerRef)\n },\n\n /**\n * dropdown positioning logic\n * (shown above/below or next to typed text for mix-mode)\n */\n position( ddHeight ){\n var _sd = this.settings.dropdown,\n appendTarget = this.dropdown.getAppendTarget();\n\n if( _sd.position == 'manual' || !appendTarget) return\n\n var rect, top, bottom, left, width, ancestorsOffsets,\n isPlacedAbove, hasSpaceOnRight,\n cssTop, cssLeft,\n ddElm = this.DOM.dropdown,\n isRTL = _sd.RTL,\n isDefaultAppendTarget = appendTarget === document.body,\n isSelfAppended = appendTarget === this.DOM.scope,\n appendTargetScrollTop = isDefaultAppendTarget ? window.pageYOffset : appendTarget.scrollTop,\n root = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement,\n viewportHeight = root.clientHeight,\n viewportWidth = Math.max(root.clientWidth || 0, window.innerWidth || 0),\n positionTo = viewportWidth > 480 ? _sd.position : 'all',\n ddTarget = this.DOM[positionTo == 'input' ? 'input' : 'scope'],\n MIN_DISTANCE_FROM_VIEWPORT_H_EDGE = 120;\n\n ddHeight = ddHeight || ddElm.clientHeight\n\n function getAncestorsOffsets(p){\n var top = 0, left = 0;\n\n p = p.parentNode;\n\n // when in element-fullscreen mode, do not go above the fullscreened-element\n while(p && p != root){\n top += p.offsetTop || 0\n left += p.offsetLeft || 0\n p = p.parentNode\n }\n\n return {top, left};\n }\n\n function getAccumulatedAncestorsScrollTop() {\n var scrollTop = 0,\n p = _sd.appendTarget.parentNode;\n\n while(p){\n scrollTop += p.scrollTop || 0;\n p = p.parentNode\n }\n\n return scrollTop;\n }\n\n if( !this.state.dropdown.visible ) return\n\n if( positionTo == 'text' ){\n rect = getCaretGlobalPosition()\n bottom = rect.bottom\n top = rect.top\n left = rect.left\n width = 'auto'\n }\n\n else{\n ancestorsOffsets = getAncestorsOffsets(appendTarget)\n rect = ddTarget.getBoundingClientRect()\n top = isSelfAppended ? -1 : rect.top - ancestorsOffsets.top\n bottom = (isSelfAppended ? rect.height : rect.bottom - ancestorsOffsets.top) - 1\n left = isSelfAppended ? -1 : rect.left - ancestorsOffsets.left\n width = rect.width + 'px'\n }\n\n // if the \"append target\" isn't the default, correct the `top` variable by ignoring any scrollTop of the target's Ancestors\n if( !isDefaultAppendTarget ) {\n let accumulatedAncestorsScrollTop = getAccumulatedAncestorsScrollTop()\n top += accumulatedAncestorsScrollTop\n bottom += accumulatedAncestorsScrollTop\n }\n\n top = Math.floor(top)\n bottom = Math.ceil(bottom)\n\n isPlacedAbove = _sd.placeAbove ?? viewportHeight - rect.bottom < ddHeight\n hasSpaceOnRight = viewportWidth - left < MIN_DISTANCE_FROM_VIEWPORT_H_EDGE;\n\n // flip vertically if there is no space for the dropdown below the input\n cssTop = (isPlacedAbove ? top : bottom) + appendTargetScrollTop;\n\n // \"pageXOffset\" property is an alias for \"scrollX\"\n cssLeft = (left + (isRTL ? (rect.width || 0) : 0) + window.pageXOffset);\n\n // check if there's enough space on the right-side of the viewport,\n // because the element is positioned to the right of the caret, which might need to be changed.\n if( positionTo == 'text' && hasSpaceOnRight ) {\n cssLeft = `right: 0;`;\n }\n else {\n cssLeft = `left: ${cssLeft}px;`;\n }\n\n // rtl = rtl ?? viewportWidth -\n ddElm.style.cssText = `${cssLeft} top: ${cssTop}px; min-width: ${width}; max-width: ${width}`;\n\n ddElm.setAttribute('placement', isPlacedAbove ? 'top' : 'bottom')\n ddElm.setAttribute('position', positionTo)\n },\n}\n", "import { isObject, escapeHTML, extend, unaccent, logger } from './helpers'\n\n\n/**\n * Tagify's dropdown suggestions-related logic\n */\n\nexport default {\n events : {\n /**\n * Events should only be binded when the dropdown is rendered and removed when isn't\n * because there might be multiple Tagify instances on a certain page\n * @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events]\n */\n binding( bindUnbind = true ){\n // references to the \".bind()\" methods must be saved so they could be unbinded later\n var _CB = this.dropdown.events.callbacks,\n // callback-refs\n _CBR = (this.listeners.dropdown = this.listeners.dropdown || {\n position : this.dropdown.position.bind(this, null),\n onKeyDown : _CB.onKeyDown.bind(this),\n onMouseOver : _CB.onMouseOver.bind(this),\n onMouseLeave : _CB.onMouseLeave.bind(this),\n onClick : _CB.onClick.bind(this),\n onScroll : _CB.onScroll.bind(this),\n }),\n action = bindUnbind ? 'addEventListener' : 'removeEventListener';\n\n if( this.settings.dropdown.position != 'manual' ){\n document[action]('scroll', _CBR.position, true)\n window[action]('resize', _CBR.position)\n window[action]('keydown', _CBR.onKeyDown)\n }\n\n this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver)\n this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave)\n this.DOM.dropdown[action]('mousedown', _CBR.onClick)\n this.DOM.dropdown.content[action]('scroll', _CBR.onScroll)\n },\n\n callbacks : {\n onKeyDown(e){\n // ignore keys during IME composition\n if( !this.state.hasFocus || this.state.composing )\n return\n\n // get the \"active\" element, and if there was none (yet) active, use first child\n var _s = this.settings,\n includeSelectedTags = _s.dropdown.includeSelectedTags,\n selectedElm = this.DOM.dropdown.querySelector(_s.classNames.dropdownItemActiveSelector),\n selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm),\n isMixMode = _s.mode == 'mix',\n isSelectMode = _s.mode == 'select';\n\n _s.hooks.beforeKeyDown(e, {tagify:this})\n .then(result => {\n switch( e.key ){\n case 'ArrowDown' :\n case 'ArrowUp' :\n case 'Down' : // >IE11\n case 'Up' : { // >IE11\n e.preventDefault()\n var dropdownItems = this.dropdown.getAllSuggestionsRefs(),\n actionUp = e.key == 'ArrowUp' || e.key == 'Up';\n\n if( selectedElm ) {\n selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp)\n }\n\n // if no element was found OR current item is not a \"real\" item, loop\n if( !selectedElm || !selectedElm.matches(_s.classNames.dropdownItemSelector) ){\n selectedElm = dropdownItems[actionUp ? dropdownItems.length - 1 : 0];\n }\n\n this.dropdown.highlightOption(selectedElm, true)\n // selectedElm.scrollIntoView({inline: 'nearest', behavior: 'smooth'})\n break;\n }\n case 'PageUp':\n case 'PageDown': {\n e.preventDefault()\n const dropdownItems = this.dropdown.getAllSuggestionsRefs()\n const itemsPerPage = Math.floor(this.DOM.dropdown.content.clientHeight / dropdownItems[0]?.offsetHeight) || 1\n const isPageUp = e.key === 'PageUp'\n\n if (selectedElm) {\n const currentIndex = dropdownItems.indexOf(selectedElm)\n const targetIndex = isPageUp\n ? Math.max(0, currentIndex - itemsPerPage)\n : Math.min(dropdownItems.length - 1, currentIndex + itemsPerPage)\n selectedElm = dropdownItems[targetIndex]\n } else {\n selectedElm = dropdownItems[0]\n }\n\n this.dropdown.highlightOption(selectedElm, true)\n break;\n }\n case 'Home':\n case 'End': {\n e.preventDefault()\n const dropdownItems = this.dropdown.getAllSuggestionsRefs()\n selectedElm = dropdownItems[e.key === 'Home' ? 0 : dropdownItems.length - 1]\n this.dropdown.highlightOption(selectedElm, true)\n break;\n }\n case 'Escape' :\n case 'Esc': // IE11\n this.dropdown.hide();\n break;\n\n case 'ArrowRight' :\n // do not continue if the left arrow key was pressed while typing, because assuming the user wants to bypass any of the below logic and edit the content without intervention.\n // also do not procceed if a tag should be created when the setting `autoComplete.rightKey` is set to `true`.\n // also do not procceed if the caret is between tags and the user is trying to move the caret to the right\n if( this.state.actions.ArrowLeft || _s.autoComplete.rightKey || _s.allowCaretBetweenTags )\n return\n case 'Tab' : {\n let shouldAutocompleteOnKey = !_s.autoComplete.rightKey || !_s.autoComplete.tabKey\n\n // in mix-mode, treat arrowRight like Enter key, so a tag will be created\n if( !isMixMode && !isSelectMode && selectedElm && shouldAutocompleteOnKey && !this.state.editing && selectedElmData ){\n e.preventDefault() // prevents blur so the autocomplete suggestion will not become a tag\n var value = this.dropdown.getMappedValue(selectedElmData)\n\n this.state.autoCompleteData = selectedElmData;\n this.input.autocomplete.set.call(this, value)\n return false\n }\n return true\n }\n case 'Enter' : {\n e.preventDefault()\n\n // temporary set the \"actions\" state to indicate to the main \"blur\" event it shouldn't execute any if its logic.\n // a `100ms` is a good-enough timeout after some testing\n this.state.actions.selectOption = true;\n setTimeout(()=> this.state.actions.selectOption = false, 100)\n\n _s.hooks.suggestionClick(e, {tagify:this, tagData:selectedElmData, suggestionElm:selectedElm})\n .then(() => {\n if( selectedElm ){\n var nextOrPrevOption = includeSelectedTags ? selectedElm : this.dropdown.getNextOrPrevOption(selectedElm, !actionUp);\n\n this.dropdown.selectOption(selectedElm, e, () => {\n // highlight next option\n if(nextOrPrevOption) {\n var nextOrPrevOptionValue = nextOrPrevOption.getAttribute('value')\n nextOrPrevOption = this.dropdown.getSuggestionNodeByValue(nextOrPrevOptionValue)\n this.dropdown.highlightOption(nextOrPrevOption)\n }\n })\n\n return\n }\n else\n this.dropdown.hide()\n\n if( !isMixMode )\n this.addTags(this.state.inputText.trim(), true)\n })\n .catch(err => logger.warn(err))\n\n break;\n }\n case 'Backspace' : {\n if( isMixMode || this.state.editing.scope ) return;\n\n const value = this.input.raw.call(this)\n\n if( value == \"\" || value.charCodeAt(0) == 8203 ){\n if( _s.backspace === true )\n this.removeTags()\n else if( _s.backspace == 'edit' )\n setTimeout(this.editTag.bind(this), 0)\n }\n }\n }\n })\n },\n\n onMouseOver(e){\n var ddItem = e.target.closest(this.settings.classNames.dropdownItemSelector)\n // event delegation check\n this.dropdown.highlightOption(ddItem)\n },\n\n onMouseLeave(e){\n // de-highlight any previously highlighted option\n this.dropdown.highlightOption()\n },\n\n onClick(e){\n if( e.button != 0 || e.target == this.DOM.dropdown || e.target == this.DOM.dropdown.content ) return; // allow only mouse left-clicks\n\n var selectedElm = e.target.closest(this.settings.classNames.dropdownItemSelector),\n selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm)\n\n // temporary set the \"actions\" state to indicate to the main \"blur\" event it shouldn't execute any if its logic.\n // a `100ms` is a good-enough timeout after some testing\n this.state.actions.selectOption = true;\n setTimeout(()=> this.state.actions.selectOption = false, 100)\n\n this.settings.hooks.suggestionClick(e, {tagify:this, tagData:selectedElmData, suggestionElm:selectedElm})\n .then(() => {\n if( selectedElm )\n this.dropdown.selectOption(selectedElm, e)\n else\n this.dropdown.hide()\n })\n .catch(err => logger.warn(err))\n },\n\n onScroll(e){\n var elm = e.target,\n pos = elm.scrollTop / (elm.scrollHeight - elm.parentNode.clientHeight) * 100;\n\n this.trigger(\"dropdown:scroll\", {percentage:Math.round(pos)})\n },\n }\n },\n\n /**\n * fill data into the suggestions list\n * (mainly used to update the list when removing tags while the suggestions dropdown is visible, so they will be re-added to the list. not efficient)\n */\n refilter( value ){\n value = value || this.state.dropdown.query || ''\n this.suggestedListItems = this.dropdown.filterListItems(value)\n\n this.dropdown.fill()\n\n if( !this.suggestedListItems.length )\n this.dropdown.hide()\n\n this.trigger(\"dropdown:updated\", this.DOM.dropdown)\n },\n\n /**\n * Given a suggestion-item, return the data associated with it\n * @param {HTMLElement} tagElm\n * @returns Object\n */\n getSuggestionDataByNode( tagElm ){\n var item, value = tagElm && tagElm.getAttribute('value')\n\n for(var i = this.suggestedListItems.length; i--; ) {\n item = this.suggestedListItems[i]\n if( isObject(item) && item.value == value ) return item\n // for primitive whitelist items:\n else if( item == value ) return {value: item}\n }\n },\n\n getSuggestionNodeByValue( value ){\n var dropdownItems = this.dropdown.getAllSuggestionsRefs()\n return dropdownItems.find(item => item.getAttribute('value') === value);\n },\n\n getNextOrPrevOption(selected, next = true) {\n var dropdownItems = this.dropdown.getAllSuggestionsRefs(),\n selectedIdx = dropdownItems.findIndex(item => item === selected);\n\n return next ? dropdownItems[selectedIdx + 1] : dropdownItems[selectedIdx - 1]\n },\n\n /**\n * mark the currently active suggestion option\n * @param {Object} elm option DOM node\n * @param {Boolean} adjustScroll when navigation with keyboard arrows (up/down), auto-scroll to always show the highlighted element\n */\n highlightOption( elm, adjustScroll ){\n var className = this.settings.classNames.dropdownItemActive,\n itemData;\n\n // focus casues a bug in Firefox with the placeholder been shown on the input element\n // if( this.settings.dropdown.position != 'manual' )\n // elm.focus();\n\n if( this.state.ddItemElm ){\n this.state.ddItemElm.classList.remove(className)\n this.state.ddItemElm.removeAttribute(\"aria-selected\")\n }\n\n if( !elm ){\n this.state.ddItemData = null\n this.state.ddItemElm = null\n this.input.autocomplete.suggest.call(this)\n return;\n }\n\n itemData = this.dropdown.getSuggestionDataByNode(elm)\n this.state.ddItemData = itemData\n this.state.ddItemElm = elm\n\n // this.DOM.dropdown.querySelectorAll(\".\" + this.settings.classNames.dropdownItemActive).forEach(activeElm => activeElm.classList.remove(className));\n elm.classList.add(className);\n elm.setAttribute(\"aria-selected\", true)\n\n if( adjustScroll )\n elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight\n\n // Try to autocomplete the typed value with the currently highlighted dropdown item\n if( this.settings.autoComplete ){\n this.input.autocomplete.suggest.call(this, itemData)\n this.dropdown.position() // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line\n }\n },\n\n /**\n * Create a tag from the currently active suggestion option\n * @param {Object} elm DOM node to select\n * @param {Object} event The original Click event, if available (since keyboard ENTER key also triggers this method)\n */\n selectOption( elm, event, onSelect ){\n var _s = this.settings,\n includeSelectedTags = _s.dropdown.includeSelectedTags,\n {clearOnSelect, closeOnSelect} = _s.dropdown;\n\n if( !elm ) {\n this.addTags(this.state.inputText, true)\n closeOnSelect && this.dropdown.hide()\n return;\n }\n\n event = event || {}\n\n // if in edit-mode, do not continue but instead replace the tag's text.\n // the scenario is that \"addTags\" was called from a dropdown suggested option selected while editing\n\n var value = elm.getAttribute('value'),\n isNoMatch = value == 'noMatch',\n isMixMode = _s.mode == 'mix',\n tagData = this.suggestedListItems.find(item => (item.value ?? item) == value)\n\n // The below event must be triggered, regardless of anything else which might go wrong\n this.trigger('dropdown:select', {data:tagData, elm, event})\n\n if( !tagData && !isNoMatch ){\n closeOnSelect && setTimeout(this.dropdown.hide.bind(this))\n return\n }\n\n if( this.state.editing ) {\n let normalizedTagData = this.normalizeTags([tagData])[0]\n tagData = _s.transformTag.call(this, normalizedTagData) || normalizedTagData\n\n // normalizing value, because \"tagData\" might be a string, and therefore will not be able to extend the object\n this.onEditTagDone(null, extend({__isValid: true}, tagData))\n }\n // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing\n else {\n this[isMixMode ? \"addMixTags\" : \"addTags\"]([tagData || this.input.raw.call(this)], clearOnSelect)\n }\n\n if( !isMixMode && !this.DOM.input.parentNode )\n return\n\n setTimeout(() => {\n this.DOM.input.focus()\n this.toggleFocusClass(true)\n })\n\n closeOnSelect && setTimeout(this.dropdown.hide.bind(this))\n\n // execute these tasks once a suggestion has been selected\n if(includeSelectedTags) {\n onSelect && onSelect()\n }\n\n // if the selected suggestion is removed after being selected, more things things needs to be done:\n else {\n elm.addEventListener('transitionend', () => {\n this.dropdown.fillHeaderFooter()\n\n setTimeout(() => {\n elm.remove()\n this.dropdown.refilter()\n onSelect && onSelect()\n }, 100)\n }, {once: true})\n\n // hide selected suggestion\n elm.classList.add(this.settings.classNames.dropdownItemHidden)\n }\n },\n\n // adds all the suggested items, including the ones which are not currently rendered,\n // unless specified otherwise (by the \"onlyRendered\" argument)\n selectAll( onlyRendered ){\n // having suggestedListItems with items messes with \"normalizeTags\" when wanting\n // to add all tags\n this.suggestedListItems.length = 0;\n this.dropdown.hide()\n\n this.dropdown.filterListItems('');\n\n var tagsToAdd = this.dropdown.filterListItems('');\n\n if( !onlyRendered )\n tagsToAdd = this.state.dropdown.suggestions\n\n // some whitelist items might have already been added as tags so when addings all of them,\n // skip adding already-added ones, so best to use \"filterListItems\" method over \"settings.whitelist\"\n this.addTags(tagsToAdd, true)\n return this\n },\n\n /**\n * returns an HTML string of the suggestions' list items\n * @param {String} value string t\n * o filter the whitelist by\n * @param {Object} options \"exact\" - for exact complete match\n * @return {Array} list of filtered whitelist items according to the settings provided and current value\n */\n filterListItems( value, options ){\n var _s = this.settings,\n _sd = _s.dropdown,\n options = options || {},\n list = [],\n exactMatchesList = [],\n whitelist = _s.whitelist,\n suggestionsCount = _sd.maxItems >= 0 ? _sd.maxItems : Infinity,\n includeSelectedTags = _sd.includeSelectedTags,\n hasCustomSort = typeof _sd.sortby == 'function',\n searchKeys = _sd.searchKeys,\n whitelistItem,\n valueIsInWhitelist,\n searchBy,\n isDuplicate,\n niddle,\n i = 0;\n\n value = (_s.mode == 'select' && this.value.length && this.value[0][_s.tagTextProp] == value\n ? '' // do not filter if the tag, which is already selecetd in \"select\" mode, is the same as the typed text\n : value);\n\n if( !value || !searchKeys.length ){\n list = includeSelectedTags\n ? whitelist\n : whitelist.filter(item => !this.isTagDuplicate( isObject(item) ? item.value : item )) // don't include tags which have already been added.\n\n var filteredList = hasCustomSort\n ? _sd.sortby(list, niddle)\n : list.slice(0, suggestionsCount); // respect \"maxItems\" dropdown setting\n\n this.state.dropdown.suggestions = filteredList;\n\n return filteredList;\n }\n\n niddle = _sd.caseSensitive\n ? \"\"+value\n : (\"\"+value).toLowerCase()\n\n // checks if ALL of the words in the search query exists in the current whitelist item, regardless of their order\n function stringHasAll(s, query){\n return query.toLowerCase().split(' ').every(q => s.includes(q.toLowerCase()))\n }\n\n for( ; i < whitelist.length; i++ ){\n let startsWithMatch, exactMatch;\n\n whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value:whitelist[i] } //normalize value as an Object\n\n let itemWithoutSearchKeys = !Object.keys(whitelistItem).some(k => searchKeys.includes(k) ),\n _searchKeys = itemWithoutSearchKeys ? [\"value\"] : searchKeys\n\n if( _sd.fuzzySearch && !options.exact ){\n searchBy = _searchKeys.reduce((values, k) => values + \" \" + (whitelistItem[k]||\"\"), \"\").toLowerCase().trim()\n\n if( _sd.accentedSearch ){\n searchBy = unaccent(searchBy)\n niddle = unaccent(niddle)\n }\n\n startsWithMatch = searchBy.indexOf(niddle) == 0\n exactMatch = searchBy === niddle\n valueIsInWhitelist = stringHasAll(searchBy, niddle)\n }\n\n else {\n startsWithMatch = true;\n valueIsInWhitelist = _searchKeys.some(k => {\n var v = '' + (whitelistItem[k] || '') // if key exists, cast to type String\n\n if( _sd.accentedSearch ){\n v = unaccent(v)\n niddle = unaccent(niddle)\n }\n\n if( !_sd.caseSensitive )\n v = v.toLowerCase()\n\n exactMatch = v === niddle\n\n return options.exact\n ? v === niddle\n : v.indexOf(niddle) == 0\n })\n }\n\n isDuplicate = !_sd.includeSelectedTags && this.isTagDuplicate( isObject(whitelistItem) ? whitelistItem.value : whitelistItem )\n\n // match for the value within each \"whitelist\" item\n if( valueIsInWhitelist && !isDuplicate )\n if( exactMatch && startsWithMatch)\n exactMatchesList.push(whitelistItem)\n else if( _sd.sortby == 'startsWith' && startsWithMatch )\n list.unshift(whitelistItem)\n else\n list.push(whitelistItem)\n }\n\n this.state.dropdown.suggestions = exactMatchesList.concat(list);\n\n // custom sorting function\n var filteredList = hasCustomSort\n ? _sd.sortby(exactMatchesList.concat(list), niddle)\n : exactMatchesList.concat(list).slice(0, suggestionsCount)\n\n this.state.dropdown.suggestions = filteredList;\n return filteredList\n },\n\n /**\n * Returns the final value of a tag data (object) with regards to the \"mapValueTo\" dropdown setting\n * @param {Object} tagData\n * @returns\n */\n getMappedValue(tagData){\n var mapValueTo = this.settings.dropdown.mapValueTo,\n value = (mapValueTo\n ? typeof mapValueTo == 'function' ? mapValueTo(tagData) : (tagData[mapValueTo] || tagData.value)\n : tagData.value);\n\n return value\n },\n\n /**\n * Creates the dropdown items' HTML\n * @param {Array} sugegstionsList [Array of Objects]\n * @return {String}\n */\n createListHTML( sugegstionsList ){\n return extend([], sugegstionsList).map((suggestion, idx) => {\n if( typeof suggestion == 'string' || typeof suggestion == 'number' )\n suggestion = {value:suggestion}\n\n var mappedValue = this.dropdown.getMappedValue(suggestion);\n\n mappedValue = (typeof mappedValue == 'string' && this.settings.dropdown.escapeHTML)\n ? escapeHTML(mappedValue)\n : mappedValue;\n\n return this.settings.templates.dropdownItem.apply(this, [{...suggestion, mappedValue}, this])\n }).join(\"\")\n }\n}", "const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped.\nconst STORE_KEY = '@yaireo/tagify/'\n\nexport const getPersistedData = id => key => {\n if( !id ) return;\n\n // if \"persist\" is \"false\", do not save to localstorage\n let customKey = '/'+key,\n persistedData,\n currentStorageVersion = localStorage?.getItem(STORE_KEY + id + '/v')\n\n if( currentStorageVersion === VERSION){\n try{ persistedData = JSON.parse(localStorage[STORE_KEY + id + customKey]) }\n catch(err){}\n }\n\n return persistedData\n}\n\nexport const setPersistedData = id => {\n if( !id ) return () => {};\n\n // for storage invalidation\n localStorage?.setItem(STORE_KEY + id + '/v', VERSION)\n\n return (data, key) => {\n let customKey = '/'+key,\n persistedData = JSON.stringify(data)\n\n if( data && key ){\n localStorage?.setItem(STORE_KEY + id + customKey, persistedData)\n dispatchEvent( new Event('storage') )\n }\n }\n}\n\nexport const clearPersistedData = id => key => {\n const base = STORE_KEY + '/' + id + '/';\n\n // delete specific key in the storage\n if( key )\n localStorage.removeItem(base + key)\n\n // delete all keys in the storage with a specific tagify id\n else {\n for(let k in localStorage)\n if( k.includes(base) )\n localStorage.removeItem(k)\n }\n}\n", "export default {\r\n empty : \"empty\",\r\n exceed : \"number of tags exceeded\",\r\n pattern : \"pattern mismatch\",\r\n duplicate : \"already exists\",\r\n notAllowed : \"not allowed\"\r\n}", "import {ZERO_WIDTH_UNICODE_CHAR} from './constants'\n\nexport default {\n /**\n *\n * @param {DOM Object} input Original input DOm element\n * @param {Object} settings Tagify instance settings Object\n */\n wrapper(input, _s){\n return `\n ${this.settings.templates.input.call(this)}\n ${ZERO_WIDTH_UNICODE_CHAR}\n `\n },\n\n input() {\n var _s = this.settings,\n placeholder = _s.placeholder || ZERO_WIDTH_UNICODE_CHAR;\n\n return ``\n },\n\n tag(tagData, {settings: _s}){\n return `\n \n
\n ${tagData[_s.tagTextProp] || tagData.value}\n
\n
`\n },\n\n dropdown(settings){\n var _sd = settings.dropdown,\n isManual = _sd.position == 'manual';\n\n return `
\n
\n
`\n },\n\n dropdownContent(HTMLContent) {\n var _t = this.settings.templates,\n suggestions = this.state.dropdown.suggestions;\n\n return `\n ${_t.dropdownHeader.call(this, suggestions)}\n ${HTMLContent}\n ${_t.dropdownFooter.call(this, suggestions)}\n `\n },\n\n dropdownItem(item){\n return `
${item.mappedValue || item.value}
`\n },\n\n /**\n * @param {Array} suggestions An array of all the matched suggested items, including those which were sliced away due to the \"dropdown.maxItems\" setting\n */\n dropdownHeader(suggestions){\n return `
`\n },\n\n dropdownFooter(suggestions){\n var hasMore = suggestions.length - this.settings.dropdown.maxItems;\n\n return hasMore > 0\n ? ``\n : '';\n },\n\n dropdownItemNoMatch: null\n}\n", "import { extend, logger } from './helpers'\r\n\r\nexport default function EventDispatcher( instance ){\r\n // Create a DOM EventTarget object\r\n var target = document.createTextNode(''),\r\n // keep track of all binded events & their callbacks to be able to completely remove all listeners of a speicific type\r\n callbacksPerType = {}\r\n\r\n function addRemove(op, events, cb){\r\n if( cb )\r\n events.split(/\\s+/g).forEach(ev => target[op + 'EventListener'].call(target, ev, cb))\r\n }\r\n\r\n // Pass EventTarget interface calls to DOM EventTarget object\r\n return {\r\n // unbinds all events\r\n removeAllCustomListeners(){\r\n Object.entries(callbacksPerType).forEach(([ev, cbArr]) => {\r\n cbArr.forEach(cb => addRemove('remove', ev, cb))\r\n })\r\n\r\n callbacksPerType = {}\r\n },\r\n\r\n off(events, cb){\r\n if( events ) {\r\n if( cb )\r\n addRemove('remove', events, cb)\r\n else\r\n // if `cb` argument was not specified then remove all listeners for the given event(s) types\r\n events.split(/\\s+/g).forEach(ev => {\r\n callbacksPerType[ev]?.forEach(cb => addRemove('remove', ev, cb))\r\n delete callbacksPerType[ev]\r\n })\r\n }\r\n\r\n return this\r\n },\r\n\r\n on(events, cb){\r\n if(cb && typeof cb == 'function') {\r\n //track events callbacks to be able to remove them altogehter\r\n events.split(/\\s+/g).forEach(ev => {\r\n if (Array.isArray(callbacksPerType[ev]) )\r\n callbacksPerType[ev].push(cb)\r\n else\r\n callbacksPerType[ev] = [cb]\r\n })\r\n\r\n addRemove('add', events, cb)\r\n }\r\n\r\n return this\r\n },\r\n\r\n trigger(eventName, data, opts){\r\n var e;\r\n\r\n opts = opts || {\r\n cloneData:true\r\n }\r\n\r\n if( !eventName ) return;\r\n\r\n if( instance.settings.isJQueryPlugin ){\r\n if( eventName == 'remove' ) eventName = 'removeTag' // issue #222\r\n jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data])\r\n }\r\n else{\r\n try {\r\n var eventData = typeof data === 'object'\r\n ? data\r\n : {value:data};\r\n\r\n eventData = opts.cloneData ? extend({}, eventData) : eventData\r\n eventData.tagify = this\r\n\r\n if( data.event )\r\n eventData.event = this.cloneEvent(data.event)\r\n\r\n // TODO: move the below to the \"extend\" function\r\n if( data instanceof Object )\r\n for( var prop in data )\r\n if(data[prop] instanceof HTMLElement)\r\n eventData[prop] = data[prop]\r\n\r\n e = new CustomEvent(eventName, {\"detail\":eventData})\r\n }\r\n catch(err){ logger.warn(err) }\r\n\r\n target.dispatchEvent(e);\r\n }\r\n }\r\n }\r\n}\r\n", "import { decode, extend, getfirstTextNode, isChromeAndroidBrowser, isNodeTag, isWithinNodeTag, injectAtCaret, getSetTagData, fixCaretBetweenTags, placeCaretAfterNode } from './helpers'\nimport {ZERO_WIDTH_CHAR} from './constants'\n\nexport function triggerChangeEvent(){\n if( this.settings.mixMode.integrated ) return;\n\n var inputElm = this.DOM.originalInput,\n changed = this.state.lastOriginalValueReported !== inputElm.value,\n event = new CustomEvent(\"change\", {bubbles: true}); // must use \"CustomEvent\" and not \"Event\" to support IE\n\n if( !changed ) return;\n\n // must apply this BEFORE triggering the simulated event\n this.state.lastOriginalValueReported = inputElm.value\n\n // React hack: https://github.com/facebook/react/issues/11488\n event.simulated = true\n if (inputElm._valueTracker)\n inputElm._valueTracker.setValue(Math.random())\n\n inputElm.dispatchEvent(event)\n\n // also trigger a Tagify event\n this.trigger(\"change\", this.state.lastOriginalValueReported)\n\n // React, for some reason, clears the input's value after \"dispatchEvent\" is fired\n inputElm.value = this.state.lastOriginalValueReported\n}\n\nexport default {\n // bind custom events which were passed in the settings\n customBinding(){\n this.customEventsList.forEach(name => {\n this.on(name, this.settings.callbacks[name])\n })\n },\n\n binding( bindUnbind = true ){\n var _s = this.settings,\n _CB = this.events.callbacks,\n _CBR,\n action = bindUnbind ? 'addEventListener' : 'removeEventListener';\n\n // do not allow the main events to be bound more than once\n if( (this.state.mainEvents && bindUnbind ) || _s.disabled || _s.readonly )\n return;\n\n\n // set the binding state of the main events, so they will not be bound more than once\n this.state.mainEvents = bindUnbind;\n\n // everything inside gets executed only once-per instance\n if( bindUnbind && !this.listeners.main ){\n this.events.bindGlobal.call(this);\n\n if( this.settings.isJQueryPlugin )\n jQuery(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))\n }\n\n // setup callback references so events could be removed later\n _CBR = (this.listeners.main = this.listeners.main || {\n keydown : ['input', _CB.onKeydown.bind(this)],\n click : ['scope', _CB.onClickScope.bind(this)],\n dblclick : _s.mode != 'select' && ['scope', _CB.onDoubleClickScope.bind(this)],\n paste : ['input', _CB.onPaste.bind(this)],\n drop : ['input', _CB.onDrop.bind(this)],\n compositionstart : ['input', _CB.onCompositionStart.bind(this)],\n compositionend : ['input', _CB.onCompositionEnd.bind(this)]\n })\n\n for( var eventName in _CBR ){\n _CBR[eventName] && this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);\n }\n\n // observers\n var inputMutationObserver = this.listeners.main.inputMutationObserver || new MutationObserver(_CB.onInputDOMChange.bind(this));\n\n // cleaup just-in-case\n inputMutationObserver.disconnect()\n\n // observe stuff\n if( _s.mode == 'mix' ) {\n inputMutationObserver.observe(this.DOM.input, {childList:true})\n }\n\n this.events.bindOriginaInputListener.call(this)\n\n if( bindUnbind ) {\n this.listeners.main = undefined\n }\n },\n\n bindOriginaInputListener(delay) {\n const DELAY = (delay||0) + 500\n\n if(!this.listeners.main) return\n // listen to original input changes (unfortunetly this is the best way...)\n // https://stackoverflow.com/a/1949416/104380\n clearInterval(this.listeners.main.originalInputValueObserverInterval)\n this.listeners.main.originalInputValueObserverInterval = setInterval(this.events.callbacks.observeOriginalInputValue.bind(this), DELAY)\n },\n\n bindGlobal( unbind ) {\n var _CB = this.events.callbacks,\n action = unbind ? 'removeEventListener' : 'addEventListener',\n e;\n\n if( !this.listeners || (!unbind && this.listeners.global) ) return; // do not re-bind\n\n // these events are global and should never be unbinded, unless the instance is destroyed:\n this.listeners.global = this.listeners.global || [\n {\n type: this.isIE ? 'keydown' : 'input', // IE cannot register \"input\" events on contenteditable elements, so the \"keydown\" should be used instead..\n target: this.DOM.input,\n cb: _CB[this.isIE ? 'onInputIE' : 'onInput'].bind(this)\n },\n {\n type: 'keydown',\n target: window,\n cb: _CB.onWindowKeyDown.bind(this)\n },\n {\n type: 'focusin',\n target: this.DOM.scope,\n cb: _CB.onFocusBlur.bind(this)\n },\n {\n type: 'focusout',\n target: this.DOM.scope,\n cb: _CB.onFocusBlur.bind(this)\n },\n {\n type: 'click',\n target: document,\n cb: _CB.onClickAnywhere.bind(this),\n useCapture: true\n },\n ]\n\n for( e of this.listeners.global )\n e.target[action](e.type, e.cb, !!e.useCapture);\n\n if( unbind ) {\n this.listeners.global = undefined\n }\n },\n\n unbindGlobal() {\n this.events.bindGlobal.call(this, true);\n },\n\n /**\n * DOM events callbacks\n */\n callbacks : {\n onFocusBlur(e){\n // when focusing within a tag which is in edit-mode\n var _s = this.settings,\n nodeTag = isWithinNodeTag.call(this, e.relatedTarget),\n targetIsTagNode = isNodeTag.call(this, e.target),\n isTargetXBtn = e.target.classList.contains(_s.classNames.tagX),\n isFocused = e.type == 'focusin',\n lostFocus = e.type == 'focusout';\n\n // when focusing within a tag which is in edit-mode, only and specifically on the text-part of the tag node\n // and not the X button or any other custom element thatmight be there\n // var tagTextNode = e.target?.closest(this.settings.classNames.tagTextSelector)\n\n if(isTargetXBtn && _s.mode != 'mix' && _s.focusInputOnRemove) {\n this.DOM.input.focus()\n }\n\n if( nodeTag && isFocused && (!targetIsTagNode) && !isTargetXBtn) {\n this.toggleFocusClass(this.state.hasFocus = +new Date())\n\n // only if focused within a tag's text node should the `onEditTagFocus` function be called.\n // if clicked anywhere else inside a tag, which had triggered an `focusin` event,\n // the onFocusBlur should be aborted. This part was spcifically written for `select` mode.\n // tagTextNode && this.events.callbacks.onEditTagFocus.call(this, nodeTag)\n }\n\n var text = e.target ? this.trim(this.DOM.input.textContent) : '', // a string\n currentDisplayValue = this.value?.[0]?.[_s.tagTextProp],\n ddEnabled = _s.dropdown.enabled >= 0,\n eventData = {relatedTarget:e.relatedTarget},\n isTargetSelectOption = this.state.actions.selectOption && (ddEnabled || !_s.dropdown.closeOnSelect),\n isTargetAddNewBtn = this.state.actions.addNew && ddEnabled,\n shouldAddTags;\n\n if( lostFocus ){\n // normalize input to end of scope; skip while focus moves into the portaled dropdown (avoid layout jump mid-selection)\n if( _s.mode != 'mix' ){\n var focusWentToDropdown = e.relatedTarget && this.DOM.dropdown?.contains(e.relatedTarget);\n if( !focusWentToDropdown )\n this.repositionScopeInput('reset', { focus: false })\n }\n\n if( e.relatedTarget === this.DOM.scope ){\n this.dropdown.hide()\n this.DOM.input.focus()\n return\n }\n\n this.postUpdate()\n _s.onChangeAfterBlur && this.triggerChangeEvent()\n }\n\n if( isTargetSelectOption || isTargetAddNewBtn || isTargetXBtn ) {\n return;\n }\n\n // should only loose focus at this point if the event was not generated from within a tag\n if( isFocused || nodeTag ) {\n this.state.hasFocus = +new Date()\n }\n else {\n this.state.hasFocus = false;\n }\n\n this.toggleFocusClass(this.state.hasFocus)\n\n if( _s.mode == 'mix' ){\n if( isFocused ){\n this.trigger(\"focus\", eventData)\n }\n\n else if( lostFocus ){\n this.trigger(\"blur\", eventData)\n this.loading(false)\n this.dropdown.hide()\n // reset state which needs reseting\n this.state.dropdown.visible = undefined\n this.setStateSelection()\n }\n\n return\n }\n\n if( isFocused ){\n if( !_s.focusable ) return;\n\n // if( !targetIsTagNode && _s.mode != 'select' ){\n // this.DOM.input.focus()\n // }\n\n var dropdownCanBeShown = _s.dropdown.enabled === 0 && !this.state.dropdown.visible,\n tagText = this.DOM.scope.querySelector(this.settings.classNames.tagTextSelector)\n\n this.trigger(\"focus\", eventData)\n // e.target.classList.remove('placeholder');\n if( dropdownCanBeShown && !targetIsTagNode ){ // && _s.mode != \"select\"\n this.dropdown.show(this.value.length ? '' : undefined)\n\n if(_s.mode === 'select') {\n this.setRangeAtStartEnd(false, tagText)\n }\n }\n\n return\n }\n\n else if( lostFocus ){\n this.trigger(\"blur\", eventData)\n this.loading(false)\n\n // when clicking the X button of a selected tag, it is unwanted for it to be added back\n // again in a few more lines of code (shouldAddTags && addTags)\n if( _s.mode == 'select' ) {\n if( this.value.length ) {\n let firstTagNode = this.getTagElms()[0];\n text = this.trim(firstTagNode.textContent)\n }\n\n // if nothing has changed (same display value), do not add a tag\n if( currentDisplayValue === text )\n text = ''\n }\n\n shouldAddTags = text && !this.state.actions.selectOption && _s.addTagOnBlur && _s.addTagOn.includes('blur');\n // do not add a tag if \"selectOption\" action was just fired (this means a tag was just added from the dropdown)\n shouldAddTags && this.addTags(text, true)\n }\n\n // when clicking a tag, do not consider this is a \"blur\" event\n if ( !nodeTag ) {\n this.DOM.input.removeAttribute('style')\n this.dropdown.hide()\n }\n },\n\n onCompositionStart(e){\n this.state.composing = true\n },\n\n onCompositionEnd(e){\n this.state.composing = false\n },\n\n onWindowKeyDown(e){\n var _s = this.settings,\n focusedElm = document.activeElement,\n withinTag = isWithinNodeTag.call(this, focusedElm),\n isBelong = withinTag && this.DOM.scope.contains(focusedElm),\n isInputNode = focusedElm === this.DOM.input,\n isReadyOnlyTag = isBelong && focusedElm.hasAttribute('readonly'),\n tagText = this.DOM.scope.querySelector(this.settings.classNames.tagTextSelector),\n isDropdownVisible = this.state.dropdown.visible,\n nextTag;\n\n if( !(e.key === 'Tab' && isDropdownVisible) && !this.state.hasFocus && (!isBelong || isReadyOnlyTag) || isInputNode ) return;\n\n nextTag = focusedElm.nextElementSibling;\n\n var targetIsRemoveBtn = e.target.classList.contains(_s.classNames.tagX);\n\n switch( e.key ){\n // remove tag if has focus\n case 'Backspace': {\n if( !_s.readonly && !this.state.editing ) {\n this.removeTags(focusedElm);\n (nextTag ? nextTag : this.DOM.input).focus()\n }\n\n break;\n }\n\n case 'Enter': {\n if( targetIsRemoveBtn ) {\n this.removeTags( e.target.parentNode )\n return\n }\n\n if( _s.a11y.focusableTags && isNodeTag.call(this, focusedElm) )\n setTimeout(this.editTag.bind(this), 0, focusedElm)\n\n break;\n }\n\n case 'ArrowDown' : {\n // if( _s.mode == 'select' ) // issue #333\n if( !this.state.dropdown.visible && _s.mode != 'mix' )\n this.dropdown.show()\n break;\n }\n\n case 'Tab': {\n tagText?.focus();\n break;\n }\n }\n },\n\n onKeydown(e){\n var _s = this.settings;\n\n // ignore keys during IME composition or when user input is not allowed\n if( this.state.composing || !_s.userInput )\n return\n\n if( _s.mode == 'select' && _s.enforceWhitelist && this.value.length && e.key != 'Tab' ){\n e.preventDefault()\n }\n\n var s = this.trim(e.target.textContent);\n\n this.trigger(\"keydown\", {event:e})\n\n _s.hooks.beforeKeyDown(e, {tagify:this})\n .then(result => {\n /**\n * ONLY FOR MIX-MODE:\n */\n if( _s.mode == 'mix' ){\n switch( e.key ){\n case 'Left' :\n case 'ArrowLeft' : {\n // when left arrow was pressed, set a flag so when the dropdown is shown, right-arrow will be ignored\n // because it seems likely the user wishes to use the arrows to move the caret\n this.state.actions.ArrowLeft = true\n break\n }\n\n case 'Delete':\n case 'Backspace' : {\n if( this.state.editing ) return\n\n var sel = document.getSelection(),\n deleteKeyTagDetected = e.key == 'Delete' && sel.anchorOffset == (sel.anchorNode.length || 0),\n prevAnchorSibling = sel.anchorNode.previousSibling,\n isCaretAfterTag = sel.anchorNode.nodeType == 1 || !sel.anchorOffset && prevAnchorSibling && prevAnchorSibling.nodeType == 1 && sel.anchorNode.previousSibling,\n lastInputValue = decode(this.DOM.input.innerHTML),\n lastTagElems = this.getTagElms(),\n isZWS = sel.anchorNode.length === 1 && sel.anchorNode.nodeValue == String.fromCharCode(8203),\n // isCaretInsideTag = sel.anchorNode.parentNode('.' + _s.classNames.tag),\n tagBeforeCaret,\n tagElmToBeDeleted,\n firstTextNodeBeforeTag;\n\n if( _s.backspace == 'edit' && isCaretAfterTag ){\n tagBeforeCaret = sel.anchorNode.nodeType == 1 ? null : sel.anchorNode.previousElementSibling;\n setTimeout(this.editTag.bind(this), 0, tagBeforeCaret); // timeout is needed to the last cahacrter in the edited tag won't get deleted\n e.preventDefault() // needed so the tag elm won't get deleted\n return;\n }\n\n if( isChromeAndroidBrowser() && isCaretAfterTag instanceof Element ){\n firstTextNodeBeforeTag = getfirstTextNode(isCaretAfterTag)\n\n if( !isCaretAfterTag.hasAttribute('readonly') )\n isCaretAfterTag.remove() // since this is Chrome, can safetly use this \"new\" DOM API\n\n // Android-Chrome wrongly hides the keyboard, and loses focus,\n // so this hack below is needed to regain focus at the correct place:\n this.DOM.input.focus()\n setTimeout(() => {\n placeCaretAfterNode(firstTextNodeBeforeTag)\n this.DOM.input.click()\n\n })\n\n return\n }\n\n if( sel.anchorNode.nodeName == 'BR')\n return\n\n if( (deleteKeyTagDetected || isCaretAfterTag) && sel.anchorNode.nodeType == 1 )\n if( sel.anchorOffset == 0 ) // caret is at the very begining, before a tag\n tagElmToBeDeleted = deleteKeyTagDetected // delete key pressed\n ? lastTagElems[0]\n : null;\n else\n tagElmToBeDeleted = lastTagElems[Math.min(lastTagElems.length, sel.anchorOffset) - 1]\n\n // find out if a tag *might* be a candidate for deletion, and if so, which\n else if( deleteKeyTagDetected )\n tagElmToBeDeleted = sel.anchorNode.nextElementSibling;\n\n else if( isCaretAfterTag instanceof Element )\n tagElmToBeDeleted = isCaretAfterTag;\n\n // tagElm.hasAttribute('readonly')\n if( sel.anchorNode.nodeType == 3 && // node at caret location is a Text node\n !sel.anchorNode.nodeValue && // has some text\n sel.anchorNode.previousElementSibling ) // text node has a Tag node before it\n e.preventDefault()\n\n // if backspace not allowed, do nothing\n // TODO: a better way to detect if nodes were deleted is to simply check the \"this.value\" before & after\n if( (isCaretAfterTag || deleteKeyTagDetected) && !_s.backspace ){\n e.preventDefault()\n return\n }\n\n if( sel.type != 'Range' && !sel.anchorOffset && sel.anchorNode == this.DOM.input && e.key != 'Delete' ){\n e.preventDefault()\n return\n }\n\n if( sel.type != 'Range' && tagElmToBeDeleted && tagElmToBeDeleted.hasAttribute('readonly') ){\n // allows the continuation of deletion by placing the caret on the first previous textNode.\n // since a few readonly-tags might be one after the other, iteration is needed:\n\n placeCaretAfterNode( getfirstTextNode(tagElmToBeDeleted) )\n return\n }\n\n if ( e.key == 'Delete' && isZWS && getSetTagData(sel.anchorNode.nextSibling) ) {\n this.removeTags(sel.anchorNode.nextSibling)\n }\n\n // update regarding https://github.com/yairEO/tagify/issues/762#issuecomment-786464317:\n // the bug described is more severe than the fix below, therefore I disable the fix until a solution\n // is found which work well for both cases.\n // -------\n // nodeType is \"1\" only when the caret is at the end after last tag (no text after), or before first first (no text before)\n /*\n if( this.isFirefox && sel.anchorNode.nodeType == 1 && sel.anchorOffset != 0 ){\n this.removeTags() // removes last tag by default if no parameter supplied\n // place caret inside last textNode, if exist. it's an annoying bug only in FF,\n // if the last tag is removed, and there is a textNode before it, the caret is not placed at its end\n placeCaretAfterNode( setRangeAtStartEnd(false, this.DOM.input) )\n }\n */\n\n break;\n }\n // currently commented to allow new lines in mixed-mode\n case 'Enter' : {\n e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380\n\n if(this.state.tag) return; // assume a tag has just been added, so no need to create a new line\n // https://stackoverflow.com/a/72279112/104380\n let selection = window.getSelection();\n let range = selection.getRangeAt(0);\n range.insertNode(document.createElement('br'));\n selection.collapseToEnd();\n }\n }\n\n return true\n }\n\n var isManualDropdown = _s.dropdown.position == 'manual';\n\n switch( e.key ){\n case 'Backspace' : {\n var tagBefore = this.getTagElmBeforeInput();\n\n if( _s.mode == 'select' && _s.enforceWhitelist && this.value.length)\n tagBefore && this.removeTags(tagBefore)\n\n else if( !this.state.dropdown.visible || _s.dropdown.position == 'manual' ){\n if( e.target.textContent == \"\" || s.charCodeAt(0) == 8203 ){ // 8203: ZERO WIDTH SPACE unicode\n if( _s.backspace === true )\n tagBefore && this.removeTags(tagBefore)\n else if( _s.backspace == 'edit' )\n tagBefore && setTimeout(() => this.editTag(tagBefore), 0) // timeout reason: when edited tag gets focused and the caret is placed at the end, the last character gets deletec (because of backspace)\n }\n }\n break;\n }\n\n case 'Esc' :\n case 'Escape' :\n if( this.state.dropdown.visible ) return\n e.target.blur()\n break;\n\n case 'Down' :\n case 'ArrowDown' :\n // if( _s.mode == 'select' ) // issue #333\n if( !this.state.dropdown.visible )\n this.dropdown.show()\n break;\n\n case 'ArrowLeft' : {\n if( this.repositionScopeInput('left') )\n e.preventDefault();\n break;\n }\n\n case 'ArrowRight' : {\n if( this.repositionScopeInput('right') ){\n e.preventDefault();\n break;\n }\n let tagData = this.state.inputSuggestion || this.state.ddItemData\n if( tagData && _s.autoComplete.rightKey ){\n this.addTags([tagData], true)\n return;\n }\n break;\n }\n\n case 'Tab' : {\n const shouldAddTag = _s.addTagOn.includes(e.key.toLowerCase());\n if(!shouldAddTag) {\n break;\n }\n }\n\n case 'Enter' : {\n // manual suggestion boxes are assumed to always be visible\n if( this.state.dropdown.visible && !isManualDropdown ) return\n\n var thingToAdd = this.state.autoCompleteData || s;\n\n // if nothing to add and tab key was pressed, return true (must be before preventDefault)\n if(!thingToAdd && e.key === \"Tab\") return true\n\n e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380\n // because the main \"keydown\" event is bound before the dropdown events, this will fire first and will not *yet*\n // know if an option was just selected from the dropdown menu. If an option was selected,\n // the dropdown events should handle adding the tag\n\n\n\n setTimeout(()=>{\n if( (!this.state.dropdown.visible || isManualDropdown) && !this.state.actions.selectOption && _s.addTagOn.includes(e.key.toLowerCase()) ) {\n this.addTags([thingToAdd], true)\n this.state.autoCompleteData = null\n }\n })\n }\n }\n })\n .catch(err => err)\n },\n\n onInput(e){\n this.postUpdate() // toggles \"tagify--empty\" class\n\n var _s = this.settings;\n\n if( _s.mode == 'mix' )\n return this.events.callbacks.onMixTagsInput.call(this, e);\n\n var value = this.input.normalize.call(this, undefined, {trim: false}),\n showSuggestions = value.length >= _s.dropdown.enabled,\n eventData = {value, inputElm:this.DOM.input},\n validation = this.validateTag({value});\n\n if( _s.mode == 'select' ) {\n this.toggleScopeValidation(validation)\n }\n\n eventData.isValid = validation;\n\n // for IE; since IE doesn't have an \"input\" event so \"keyDown\" is used instead to trigger the \"onInput\" callback,\n // and so many keys do not change the input, and for those do not continue.\n if( this.state.inputText == value ) return;\n\n // save the value on the input's State object\n this.input.set.call(this, value, false); // update the input with the normalized value and run validations\n // this.setRangeAtStartEnd(false, this.DOM.input); // fix caret position\n\n // if delimiters detected, add tags\n if( value.search(_s.delimiters) != -1 ){\n if( this.addTags( value ) ){\n this.input.set.call(this); // clear the input field's value\n }\n }\n\n else if( _s.dropdown.enabled >= 0 ){\n this.dropdown[showSuggestions ? \"show\" : \"hide\"](value);\n }\n\n this.trigger('input', eventData) // \"input\" event must be triggered at this point, before the dropdown is shown\n },\n\n onMixTagsInput( e ){\n var rangeText, match, matchedPatternCount, tag, showSuggestions, selection,\n _s = this.settings,\n lastTagsCount = this.value.length,\n matchFlaggedTag,\n matchDelimiters,\n tagsElems = this.getTagElms(),\n fragment = document.createDocumentFragment(),\n range = window.getSelection().getRangeAt(0),\n remainingTagsValues = [].map.call(tagsElems, node => getSetTagData(node).value);\n\n // Android Chrome \"keydown\" event argument does not report the correct \"key\".\n // this workaround is needed to manually call \"onKeydown\" method with a synthesized event object\n if( e.inputType == \"deleteContentBackward\" && isChromeAndroidBrowser() ){\n this.events.callbacks.onKeydown.call(this, {\n target: e.target,\n key: \"Backspace\",\n })\n }\n\n // if there's a tag as the first child of the input, always make sure it has a zero-width character before it\n // or if two tags are next to each-other, add a zero-space width character (For the caret to appear)\n fixCaretBetweenTags(this.getTagElms())\n\n // re-add \"readonly\" tags which might have been removed\n this.value.slice().forEach(item => {\n if( item.readonly && !remainingTagsValues.includes(item.value) )\n fragment.appendChild( this.createTagElem(item) )\n })\n\n if( fragment.childNodes.length ){\n range.insertNode(fragment)\n this.setRangeAtStartEnd(false, fragment.lastChild)\n }\n\n // check if tags were \"magically\" added/removed (browser redo/undo or CTRL-A -> delete)\n if( tagsElems.length != lastTagsCount ){\n this.value = [].map.call(this.getTagElms(), node => getSetTagData(node))\n this.update({ withoutChangeEvent:true })\n return\n }\n\n if( this.hasMaxTags() )\n return true\n\n if( window.getSelection ){\n selection = window.getSelection()\n\n // only detect tags if selection is inside a textNode (not somehow on already-existing tag)\n if( selection.rangeCount > 0 && selection.anchorNode.nodeType == 3 ){\n range = selection.getRangeAt(0).cloneRange()\n range.collapse(true)\n range.setStart(selection.focusNode, 0)\n\n rangeText = range.toString().slice(0, range.endOffset) // slice the range so everything AFTER the caret will be trimmed\n // split = range.toString().split(_s.mixTagsAllowedAfter) // [\"foo\", \"bar\", \"@baz\"]\n matchedPatternCount = rangeText.split(_s.pattern).length - 1;\n\n match = rangeText.match( _s.pattern )\n\n if( match )\n // tag string, example: \"@aaa ccc\"\n tag = rangeText.slice( rangeText.lastIndexOf(match[match.length-1]) )\n\n if( tag ){\n this.state.actions.ArrowLeft = false // start fresh, assuming the user did not (yet) used any arrow to move the caret\n this.state.tag = {\n prefix : tag.match(_s.pattern)[0],\n value : tag.replace(_s.pattern, ''), // get rid of the prefix\n }\n this.state.tag.baseOffset = selection.baseOffset - this.state.tag.value.length\n\n matchDelimiters = this.state.tag.value.match(_s.delimiters)\n // if a delimeter exists, add the value as tag (exluding the delimiter)\n if( matchDelimiters ){\n this.state.tag.value = this.state.tag.value.replace(_s.delimiters, '')\n this.state.tag.delimiters = matchDelimiters[0]\n this.addTags(this.state.tag.value, _s.dropdown.clearOnSelect)\n this.dropdown.hide()\n return\n }\n\n showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled\n\n // When writing something that might look like a tag (an email address) but isn't one - it is unwanted\n // the suggestions dropdown be shown, so the user can close it (in any way), and while continue typing,\n // dropdown should stay closed until another tag is typed.\n // if( this.state.tag.value.length && this.state.dropdown.visible === false )\n // showSuggestions = false\n\n // test for similar flagged tags to the current tag\n\n try{\n matchFlaggedTag = this.state.flaggedTags[this.state.tag.baseOffset]\n matchFlaggedTag = matchFlaggedTag.prefix == this.state.tag.prefix &&\n matchFlaggedTag.value[0] == this.state.tag.value[0]\n\n // reset\n if( this.state.flaggedTags[this.state.tag.baseOffset] && !this.state.tag.value )\n delete this.state.flaggedTags[this.state.tag.baseOffset];\n }\n catch(err){}\n\n // scenario: (do not show suggestions of another matched tag, if more than one detected)\n // (2 tags exist) \" a@a.com and @\"\n // (second tag is removed by backspace) \" a@a.com and \"\n if( matchFlaggedTag || matchedPatternCount < this.state.mixMode.matchedPatternCount )\n showSuggestions = false\n }\n // no (potential) tag found\n else{\n this.state.flaggedTags = {}\n }\n\n this.state.mixMode.matchedPatternCount = matchedPatternCount\n }\n }\n\n\n // wait until the \"this.value\" has been updated (see \"onKeydown\" method for \"mix-mode\")\n // the dropdown must be shown only after this event has been triggered, so an implementer could\n // dynamically change the whitelist.\n setTimeout(()=>{\n this.update({withoutChangeEvent:true})\n this.trigger('input', extend({}, this.state.tag, {textContent:this.DOM.input.textContent}))\n\n if( this.state.tag )\n this.dropdown[showSuggestions ? \"show\" : \"hide\"](this.state.tag.value);\n }, 10)\n },\n\n onInputIE(e){\n var _this = this;\n // for the \"e.target.textContent\" to be changed, the browser requires a small delay\n setTimeout(function(){\n _this.events.callbacks.onInput.call(_this, e)\n })\n },\n\n observeOriginalInputValue(){\n // if, for some reason, the Tagified element is no longer in the DOM,\n // call the \"destroy\" method to kill all references to timeouts/intervals\n if( !this.DOM.originalInput.parentNode ) this.destroy()\n\n // if original input value changed for some reason (for exmaple a form reset)\n if( this.DOM.originalInput.value != this.DOM.originalInput.tagifyValue )\n this.loadOriginalValues()\n },\n\n onClickAnywhere(e){\n if (e.target != this.DOM.scope && !this.DOM.scope.contains(e.target)) {\n this.toggleFocusClass(false)\n this.state.hasFocus = false\n\n let closestTagifyDropdownElem = e.target.closest(this.settings.classNames.dropdownSelector);\n\n // do not hide the dropdown if a click was initiated within it and that dropdown belongs to this Tagify instance\n if( closestTagifyDropdownElem?.__tagify != this )\n this.dropdown.hide()\n }\n },\n\n onClickScope(e){\n var _s = this.settings,\n tagElm = e.target.closest('.' + _s.classNames.tag),\n isScope = e.target === this.DOM.scope,\n timeDiffFocus = +new Date() - this.state.hasFocus;\n\n if( e.target.classList.contains(_s.classNames.tagX) ){\n this.removeTags( e.target.parentNode )\n return\n }\n\n else if( tagElm && !this.state.editing ){\n this.trigger(\"click\", { tag:tagElm, index:this.getNodeIndex(tagElm), data:getSetTagData(tagElm), event:e })\n\n if( _s.editTags === 1 || _s.editTags.clicks === 1 || _s.mode == 'select' )\n this.events.callbacks.onDoubleClickScope.call(this, e)\n\n return\n }\n\n // when clicking on the input itself\n else if( e.target == this.DOM.input ){\n if( _s.mode == 'mix' ){\n // firefox won't show caret if last element is a tag (and not a textNode),\n // so an empty textnode should be added\n this.fixFirefoxLastTagNoCaret()\n }\n\n if( timeDiffFocus > 500 || !_s.focusable ){\n if( this.state.dropdown.visible )\n this.dropdown.hide()\n else if( _s.dropdown.enabled === 0 && _s.mode != 'mix' )\n this.dropdown.show(this.value.length ? '' : undefined)\n return\n }\n }\n\n if( _s.mode == 'select' && _s.dropdown.enabled === 0 && !this.state.dropdown.visible) {\n this.events.callbacks.onDoubleClickScope.call(this, {...e, target: this.getTagElms()[0]})\n\n !_s.userInput && this.dropdown.show()\n }\n },\n\n // special proccess is needed for pasted content in order to \"clean\" it\n onPaste(e){\n e.preventDefault()\n\n var tagsElems,\n _s = this.settings;\n\n if( !_s.userInput ){\n return false;\n }\n\n var clipboardData, pastedText;\n\n if( _s.readonly ) return\n\n // Get pasted data via clipboard API\n clipboardData = e.clipboardData || window.clipboardData\n pastedText = clipboardData.getData('Text')\n\n _s.hooks.beforePaste(e, {tagify:this, pastedText, clipboardData})\n .then(result => {\n if( result === undefined )\n result = pastedText;\n\n if( result ){\n if( this.settings.mode == 'mix' ){\n // Auto-convert pasted pattern-prefixed text to tags if pasteAsTags is enabled\n if( this.settings.pasteAsTags ){\n const interpolatedText = this.convertPastedTextToMixTags(result)\n const fragment = this.parseMixTags(interpolatedText, { skipDOM: true })\n const tagsData = fragment.__tagifyTagsData || []\n\n // Inject the fragment (contains text nodes + tag elements)\n this.injectAtCaret(fragment, window.getSelection().getRangeAt(0))\n\n // Update internal state with new tags\n // Note: duplicates are already filtered out by parseMixTags based on settings\n tagsData.forEach(tagData => this.value.push(tagData))\n\n // Get the newly added tag elements and set their data\n const addedTags = this.getTagElms().slice(-tagsData.length)\n addedTags.forEach((elm, idx) => getSetTagData(elm, tagsData[idx]))\n\n this.update()\n fixCaretBetweenTags(addedTags)\n }\n else {\n this.injectAtCaret(result, window.getSelection().getRangeAt(0))\n }\n\n this.events.callbacks.onMixTagsInput.call(this, e);\n }\n\n else {\n this.injectAtCaret(result, window.getSelection().getRangeAt(0))\n\n if( this.settings.pasteAsTags ){\n tagsElems = this.addTags(this.state.inputText + result, true)\n }\n else {\n this.state.inputText = result\n this.dropdown.show(result)\n }\n }\n }\n\n this.trigger('paste', {event: e, pastedText, clipboardData, tagsElems})\n })\n .catch(err => err)\n },\n\n onDrop(e){\n e.preventDefault()\n },\n\n onEditTagInput( editableElm, e ){\n var tagElm = editableElm.closest('.' + this.settings.classNames.tag),\n tagElmIdx = this.getNodeIndex(tagElm),\n tagData = getSetTagData(tagElm),\n textValue = this.input.normalize.call(this, editableElm),\n dataForChangedProp = {[this.settings.tagTextProp]: textValue, __tagId: tagData.__tagId}, // \"__tagId\" is needed so validation will skip current tag when checking for dups\n isValid = this.validateTag(dataForChangedProp), // the value could have been invalid in the first-place so make sure to re-validate it (via \"addEmptyTag\" method)\n hasChanged = this.editTagChangeDetected(extend(tagData, dataForChangedProp));\n\n // if the value is same as before-editing and the tag was valid before as well, ignore the current \"isValid\" result, which is false-positive\n if( !hasChanged && editableElm.originalIsValid === true )\n isValid = true\n\n tagElm.classList.toggle(this.settings.classNames.tagInvalid, isValid !== true)\n tagData.__isValid = isValid\n\n tagElm.title = isValid === true\n ? tagData.title || tagData.value\n : isValid // change the tag's title to indicate why is the tag invalid (if it's so)\n\n // show dropdown if typed text is equal or more than the \"enabled\" dropdown setting\n if( textValue.length >= this.settings.dropdown.enabled ){\n // this check is needed apparently because doing browser \"undo\" will fire\n // \"onEditTagInput\" but \"this.state.editing\" will be \"false\"\n if( this.state.editing )\n this.state.editing.value = textValue\n this.dropdown.show(textValue)\n }\n\n this.trigger(\"edit:input\", {\n tag : tagElm,\n index: tagElmIdx,\n data : extend({}, this.value[tagElmIdx], {newValue:textValue}),\n event: e\n })\n },\n\n onEditTagPaste( tagElm, e ){\n // Get pasted data via clipboard API\n var clipboardData = e.clipboardData || window.clipboardData,\n pastedText = clipboardData.getData('Text');\n\n e.preventDefault()\n\n var newNode = injectAtCaret(pastedText)\n this.setRangeAtStartEnd(false, newNode)\n },\n\n onEditTagClick( tagElm, e) {\n this.events.callbacks.onClickScope.call(this, e)\n },\n\n onEditTagFocus( tagElm ){\n this.state.editing = {\n scope: tagElm,\n input: tagElm.querySelector(\"[contenteditable]\")\n }\n },\n\n onEditTagBlur( editableElm, e ){\n // if \"relatedTarget\" is the tag then do not continue as this should not be considered a \"blur\" event\n var isRelatedTargetNodeTag = isNodeTag.call(this, e.relatedTarget)\n\n // in \"select-mode\" when editing the tag's template to include more nodes other than the editable \"span\",\n // clicking those elements should not be considered a blur event\n if( this.settings.mode == 'select' && isRelatedTargetNodeTag && e.relatedTarget.contains(e.target) ) {\n this.dropdown.hide()\n return\n }\n\n // if \"ESC\" key was pressed then the \"editing\" state should be `false` and if so, logic should not continue\n // because \"ESC\" reverts the edited tag back to how it was (replace the node) before editing\n if( !this.state.editing )\n return;\n\n if( !this.state.hasFocus )\n this.toggleFocusClass()\n\n if(!this.DOM.scope.contains(document.activeElement)) {\n this.trigger(\"blur\", {})\n }\n\n // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it\n // the \"onEditTagDone\" is called directly, already replacing the tag, so the argument \"editableElm\"\n // node isn't in the DOM anynmore because it has been replaced.\n if( !this.DOM.scope.contains(editableElm) ) return;\n\n var _s = this.settings,\n tagElm = editableElm.closest('.' + _s.classNames.tag),\n tagData = getSetTagData(tagElm),\n textValue = this.input.normalize.call(this, editableElm),\n dataForChangedProp = {[_s.tagTextProp]: textValue, __tagId: tagData.__tagId}, // \"__tagId\" is needed so validation will skip current tag when checking for dups\n originalData = tagData.__originalData, // pre-edit data\n hasChanged = this.editTagChangeDetected(extend(tagData, dataForChangedProp)),\n isValid = this.validateTag(dataForChangedProp), // \"__tagId\" is needed so validation will skip current tag when checking for dups\n hasMaxTags,\n newTagData;\n\n if( !textValue ){\n this.onEditTagDone(tagElm)\n return\n }\n\n // if nothing changed revert back to how it was before editing\n if( !hasChanged ){\n this.onEditTagDone(tagElm, originalData)\n return\n }\n\n // need to know this because if \"keepInvalidTags\" setting is \"true\" and an invalid tag is edited as a valid one,\n // but the maximum number of tags have alreay been reached, so it should not allow saving the new valid value.\n // only if the tag was already valid before editing, ignore this check (see a few lines below)\n hasMaxTags = this.hasMaxTags()\n\n newTagData = extend(\n {},\n originalData,\n {\n [_s.tagTextProp]: this.trim(textValue),\n __isValid: isValid\n }\n )\n\n // pass through optional transformer defined in settings\n _s.transformTag.call(this, newTagData, originalData)\n\n // MUST re-validate after tag transformation\n // only validate the \"tagTextProp\" because is the only thing that metters for validating an edited tag.\n // -- Scenarios: --\n // 1. max 3 tags allowd. there are 4 tags, one has invalid input and is edited to a valid one, and now should be marked as \"not allowed\" because limit of tags has reached\n // 2. max 3 tags allowed. there are 3 tags, one is edited, and so max-tags vaildation should be OK\n isValid = (!hasMaxTags || originalData.__isValid === true) && this.validateTag(newTagData)\n\n if( isValid !== true ){\n this.trigger(\"invalid\", { data:newTagData, tag:tagElm, message:isValid })\n\n // do nothing if invalid, stay in edit-mode until corrected or reverted by presssing esc\n if( _s.editTags.keepInvalid ) return\n\n if( _s.keepInvalidTags )\n newTagData.__isValid = isValid\n else\n // revert back if not specified to keep\n newTagData = originalData\n }\n\n else if( _s.keepInvalidTags ){\n // cleaup any previous leftovers if the tag was invalid\n delete newTagData.title\n delete newTagData[\"aria-invalid\"]\n delete newTagData.class\n }\n\n // tagElm.classList.toggle(_s.classNames.tagInvalid, true)\n\n this.onEditTagDone(tagElm, newTagData)\n },\n\n onEditTagkeydown(e, tagElm){\n // ignore keys during IME composition\n if( this.state.composing )\n return\n\n this.trigger(\"edit:keydown\", {event:e})\n\n switch( e.key ){\n case 'Esc' :\n case 'Escape' : {\n this.state.editing = false\n var hasValueToRevertTo = !!tagElm.__tagifyTagData.__originalData.value\n\n if( hasValueToRevertTo )\n // revert the tag to how it was before editing\n // replace current tag with original one (pre-edited one)\n tagElm.parentNode.replaceChild(tagElm.__tagifyTagData.__originalHTML, tagElm)\n else\n tagElm.remove()\n\n break\n }\n case 'Enter' :\n case 'Tab' : {\n e.preventDefault()\n\n var EDITED_TAG_BLUR_DELAY = 0;\n\n // a setTimeout is used so when editing (in \"select\" mode) while the dropdown is shown and a suggestion is highlighted\n // and ENTER key is pressed down - the `dropdown.hide` method won't be invoked immediately and unbind the dropdown's\n // KEYDOWN \"ENTER\" before it has time to call the handler and select the suggestion.\n setTimeout(() => e.target.blur(), EDITED_TAG_BLUR_DELAY)\n }\n }\n },\n\n onDoubleClickScope(e){\n var tagElm = e.target.closest('.' + this.settings.classNames.tag);\n\n if( !tagElm ) return\n\n var tagData = getSetTagData(tagElm),\n _s = this.settings,\n isEditingTag,\n isReadyOnlyTag;\n\n if( tagData?.editable === false ) return\n\n isEditingTag = tagElm.classList.contains(this.settings.classNames.tagEditing)\n isReadyOnlyTag = tagElm.hasAttribute('readonly')\n\n if( !_s.readonly && !isEditingTag && !isReadyOnlyTag && this.settings.editTags && _s.userInput ) {\n this.events.callbacks.onEditTagFocus.call(this, tagElm)\n this.editTag(tagElm)\n }\n\n this.toggleFocusClass(true)\n\n if( _s.mode != 'select' )\n this.trigger('dblclick', { tag:tagElm, index:this.getNodeIndex(tagElm), data:getSetTagData(tagElm) })\n },\n\n /**\n *\n * @param {Object} m an object representing the observed DOM changes\n */\n onInputDOMChange(m){\n // get the last child BEFORE processing mutations\n var lastInputChild = this.DOM.input.lastChild;\n\n // iterate all DOM mutation\n m.forEach(record => {\n // only the ADDED nodes\n record.addedNodes.forEach(addedNode => {\n // fix chrome's placing '

' everytime ENTER key is pressed, and replace with just `
' ){\n addedNode.replaceWith(document.createElement('br'))\n }\n\n // if the added element is a div containing a tag within it (chrome does this when pressing ENTER before a tag)\n else if( addedNode.nodeType == 1 && addedNode.querySelector(this.settings.classNames.tagSelector) ){\n let newlineText = document.createTextNode('')\n\n if( addedNode.childNodes[0].nodeType == 3 && addedNode.previousSibling.nodeName != 'BR' )\n newlineText = document.createTextNode('\\n')\n\n // unwrap the useless div\n // chrome adds a BR at the end which should be removed\n addedNode.replaceWith(...[newlineText, ...[...addedNode.childNodes].slice(0,-1)])\n placeCaretAfterNode(newlineText)\n }\n\n // if this is a tag\n else if( isNodeTag.call(this, addedNode) ){\n if( addedNode.previousSibling?.nodeType == 3 && !addedNode.previousSibling.textContent )\n addedNode.previousSibling.remove()\n\n // and it is the first node in a new line\n if( addedNode.previousSibling && addedNode.previousSibling.nodeName == 'BR' ){\n // allows placing the caret just before the tag, when the tag is the first node in that line\n addedNode.previousSibling.replaceWith('\\n' + ZERO_WIDTH_CHAR)\n\n let nextNode = addedNode.nextSibling, anythingAfterNode = '';\n\n while (nextNode) {\n anythingAfterNode += nextNode.textContent\n nextNode = nextNode.nextSibling;\n }\n\n // when hitting ENTER for new line just before an existing tag, but skip below logic when a tag has been addded\n anythingAfterNode.trim() && placeCaretAfterNode(addedNode.previousSibling)\n }\n\n // if previous sibling does not exists (meanning the addedNode is the first node in this.DOM.input)\n // or, if the previous sibling is also a tag, add a zero-space character before (to allow showing the caret in Chrome)\n else if( !addedNode.previousSibling || getSetTagData(addedNode.previousSibling) ){\n addedNode.before(ZERO_WIDTH_CHAR)\n }\n }\n })\n\n record.removedNodes.forEach(removedNode => {\n // when trying to delete a tag which is in a new line and there's nothing else there (caret is after the tag)\n if( removedNode && removedNode.nodeName == 'BR' && isNodeTag.call(this, lastInputChild)){\n this.removeTags(lastInputChild)\n this.fixFirefoxLastTagNoCaret()\n }\n })\n })\n\n // get the last child only after the above DOM modifications\n // check these scenarios:\n // 1. after a single line, press ENTER once - should add only 1 BR\n // 2. presss ENTER right before a tag\n // 3. press enter within a text node before a tag\n\n\n if( lastInputChild && lastInputChild.nodeValue == '' )\n lastInputChild.remove()\n\n // make sure the last element is always a BR\n if( !lastInputChild || lastInputChild.nodeName != 'BR' ){\n this.DOM.input.appendChild(document.createElement('br'))\n }\n },\n }\n}\n\n", "import { sameStr, removeCollectionProp, omit, isObject, parseHTML, removeTextChildNodes, escapeHTML, extend, concatWithoutDups, getUID, isNodeTag, injectAtCaret, placeCaretAfterNode, getSetTagData, fixCaretBetweenTags, logger } from './parts/helpers'\nimport DEFAULTS from './parts/defaults'\nimport _dropdown, { initDropdown } from './parts/dropdown'\nimport { getPersistedData, setPersistedData, clearPersistedData } from './parts/persist'\nimport TEXTS from './parts/texts'\nimport templates from './parts/templates'\nimport EventDispatcher from './parts/EventDispatcher'\nimport events, { triggerChangeEvent } from './parts/events'\nimport { UPDATE_DELAY } from './parts/constants'\n\n/**\n * @constructor\n * @param {Object} input DOM element\n * @param {Object} settings settings object\n */\nfunction Tagify( input, settings ){\n if( !input ){\n logger.warn('input element not found', input)\n // return an empty mock of all methods, so the code using tagify will not break\n // because it might be calling methods even though the input element does not exist\n const mockInstance = new Proxy(this, { get(){ return () => mockInstance } })\n return mockInstance\n }\n\n if( input.__tagify ){\n logger.warn('input element is already Tagified - Same instance is returned.', input)\n return input.__tagify\n }\n\n extend(this, EventDispatcher(this))\n this.isFirefox = (/firefox|fxios/i).test(navigator.userAgent) && !(/seamonkey/i).test(navigator.userAgent)\n this.isIE = window.document.documentMode; // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility\n\n settings = settings || {};\n this.getPersistedData = getPersistedData(settings.id)\n this.setPersistedData = setPersistedData(settings.id)\n this.clearPersistedData = clearPersistedData(settings.id)\n this.applySettings(input, settings)\n\n this.state = {\n inputText: '',\n editing : false,\n composing: false,\n actions : {}, // UI actions for state-locking\n mixMode : {},\n dropdown: {},\n flaggedTags: {} // in mix-mode, when a string is detetced as potential tag, and the user has chocen to close the suggestions dropdown, keep the record of the tasg here\n }\n\n this.value = [] // tags' data\n\n // events' callbacks references will be stores here, so events could be unbinded\n this.listeners = {}\n\n this.DOM = {} // Store all relevant DOM elements in an Object\n\n this.build(input)\n initDropdown.call(this)\n\n this.getCSSVars()\n this.loadOriginalValues()\n\n this.events.customBinding.call(this)\n this.events.binding.call(this)\n input.autofocus && this.DOM.input.focus()\n input.__tagify = this\n}\n\nTagify.prototype = {\n _dropdown,\n placeCaretAfterNode,\n getSetTagData,\n helpers: {sameStr, removeCollectionProp, omit, isObject, parseHTML, escapeHTML, extend, concatWithoutDups, getUID, isNodeTag},\n\n customEventsList : ['change', 'add', 'remove', 'invalid', 'input', 'paste', 'click', 'keydown', 'focus', 'blur', 'edit:input', 'edit:beforeUpdate', 'edit:updated', 'edit:start', 'edit:keydown', 'dropdown:show', 'dropdown:hide', 'dropdown:select', 'dropdown:updated', 'dropdown:noMatch', 'dropdown:scroll'],\n dataProps: ['__isValid', '__removed', '__originalData', '__originalHTML', '__tagId'], // internal-uasge props\n\n trim(text){\n return this.settings.trim && text && typeof text == \"string\" ? text.trim() : text\n },\n\n // expose this handy utility function\n parseHTML,\n\n templates,\n\n parseTemplate(template, data){\n template = this.settings.templates[template] || template;\n return parseHTML( template.apply(this, data) )\n },\n\n set whitelist( arr ){\n const isArray = arr && Array.isArray(arr)\n this.settings.whitelist = isArray ? arr : []\n this.setPersistedData(isArray ? arr : [], 'whitelist')\n },\n\n get whitelist(){\n return this.settings.whitelist\n },\n\n set userInput( state ){\n this.settings.userInput = !!state\n this.setContentEditable(!!state)\n },\n\n get userInput(){\n return this.settings.userInput\n },\n\n generateClassSelectors(classNames){\n for( let name in classNames ) {\n let currentName = name;\n Object.defineProperty(classNames, currentName + \"Selector\" , {\n get(){ return \".\" + this[currentName].split(\" \")[0] }\n })\n }\n },\n\n applySettings( input, settings ){\n DEFAULTS.templates = this.templates\n\n var mixModeDefaults = {\n pasteAsTags: false,\n dropdown: {\n position: \"text\"\n }\n }\n\n var mergedDefaults = extend({}, DEFAULTS, (settings.mode == 'mix' ? mixModeDefaults : {}));\n this.origSettings = extend({}, settings);\n var _s = this.settings = extend({}, mergedDefaults, settings)\n\n _s.disabled = input.hasAttribute('disabled')\n _s.readonly = _s.readonly || input.hasAttribute('readonly')\n _s.placeholder = escapeHTML(input.getAttribute('placeholder') || _s.placeholder || \"\")\n _s.required = input.hasAttribute('required')\n\n this.generateClassSelectors(_s.classNames)\n\n if( this.isIE )\n _s.autoComplete = false; // IE goes crazy if this isn't false\n\n [\"whitelist\", \"blacklist\"].forEach(name => {\n var attrVal = input.getAttribute('data-' + name)\n if( attrVal ){\n attrVal = attrVal.split(_s.delimiters)\n if( attrVal instanceof Array )\n _s[name] = attrVal\n }\n })\n\n // backward-compatibility for old version of \"autoComplete\" setting:\n if( \"autoComplete\" in settings && !isObject(settings.autoComplete) ){\n _s.autoComplete = DEFAULTS.autoComplete\n _s.autoComplete.enabled = settings.autoComplete\n }\n\n if( _s.mode == 'mix' ){\n _s.pattern = _s.pattern || /@/;\n _s.autoComplete.rightKey = true\n _s.delimiters = settings.delimiters || null // default dlimiters in mix-mode must be NULL\n\n // needed for \"filterListItems\". This assumes the user might have forgotten to manually\n // define the same term in \"dropdown.searchKeys\" as defined in \"tagTextProp\" setting, so\n // by automatically adding it, tagify is \"helping\" out, guessing the intesntions of the developer.\n if( _s.tagTextProp && !_s.dropdown.searchKeys.includes(_s.tagTextProp) )\n _s.dropdown.searchKeys.push(_s.tagTextProp)\n }\n\n if( input.pattern )\n try { _s.pattern = new RegExp(input.pattern) }\n catch(e){}\n\n // Convert the \"delimiters\" setting into a REGEX object\n if( _s.delimiters ){\n _s._delimiters = _s.delimiters;\n try { _s.delimiters = new RegExp(this.settings.delimiters, \"g\") }\n catch(e){}\n }\n\n if( _s.disabled || _s.readonly )\n _s.userInput = false;\n\n this.TEXTS = {...TEXTS, ...(_s.texts || {})}\n\n // it makes sense to enable \"includeSelectedTags\" in \"select-mode\"\n if( _s.mode == 'select' ){\n _s.dropdown.includeSelectedTags = true\n }\n\n // make sure the dropdown will be shown on \"focus\" and not only after typing something (in \"select\" mode)\n if( (_s.mode == 'select' && !settings.dropdown?.enabled) || !_s.userInput ){\n _s.dropdown.enabled = 0\n }\n\n // additional override\n if( _s.disabled ) {\n _s.dropdown.enabled = false;\n }\n\n _s.dropdown.appendTarget = settings.dropdown?.appendTarget || document.body;\n\n if ( _s.dropdown.includeSelectedTags === undefined )\n _s.dropdown.includeSelectedTags = _s.duplicates;\n\n // get & merge persisted data with current data\n let persistedWhitelist = this.getPersistedData('whitelist');\n\n if( Array.isArray(persistedWhitelist))\n this.whitelist = Array.isArray(_s.whitelist)\n ? concatWithoutDups(_s.whitelist, persistedWhitelist)\n : persistedWhitelist;\n },\n\n /**\n * Returns a string of HTML element attributes\n * @param {Object} data [Tag data]\n */\n getAttributes( data ){\n var attrs = this.getCustomAttributes(data), s = '', k;\n\n for( k in attrs )\n s += \" \" + k + (data[k] !== undefined ? `=\"${attrs[k]}\"` : \"\");\n\n return s;\n },\n\n /**\n * Returns an object of attributes to be used for the templates\n */\n getCustomAttributes( data ){\n // only items which are objects have properties which can be used as attributes\n if( !isObject(data) )\n return '';\n\n var output = {}, propName;\n\n for( propName in data ){\n if( propName.slice(0,2) != '__' && propName != 'class' && data.hasOwnProperty(propName) && data[propName] !== undefined )\n output[propName] = escapeHTML(data[propName])\n }\n return output\n },\n\n setStateSelection(){\n var selection = window.getSelection()\n\n // save last selection place to be able to inject anything from outside to that specific place\n var sel = {\n anchorOffset: selection.anchorOffset,\n anchorNode : selection.anchorNode,\n range : selection.getRangeAt && selection.rangeCount && selection.getRangeAt(0)\n }\n\n this.state.selection = sel\n return sel\n },\n\n /**\n * Get specific CSS variables which are relevant to this script and parse them as needed.\n * The result is saved on the instance in \"this.CSSVars\"\n */\n getCSSVars(){\n var compStyle = getComputedStyle(this.DOM.scope, null)\n\n const getProp = name => compStyle.getPropertyValue('--'+name)\n\n function seprateUnitFromValue(a){\n if( !a ) return {}\n a = a.trim().split(' ')[0]\n var unit = a.split(/\\d+/g).filter(n=>n).pop().trim(),\n value = +a.split(unit).filter(n=>n)[0].trim()\n return {value, unit}\n }\n\n this.CSSVars = {\n tagHideTransition: (({value, unit}) => unit=='s' ? value * 1000 : value)(seprateUnitFromValue(getProp('tag-hide-transition')))\n }\n },\n\n /**\n * builds the HTML of this component\n * @param {Object} input [DOM element which would be \"transformed\" into \"Tags\"]\n */\n build( input ){\n var DOM = this.DOM,\n labelWrapper = input.closest('label');\n\n if( this.settings.mixMode.integrated ){\n DOM.originalInput = null;\n DOM.scope = input;\n DOM.input = input;\n }\n\n else {\n DOM.originalInput = input\n DOM.originalInput_tabIndex = input.tabIndex\n DOM.scope = this.parseTemplate('wrapper', [input, this.settings])\n DOM.input = DOM.scope.querySelector(this.settings.classNames.inputSelector)\n input.parentNode.insertBefore(DOM.scope, input)\n input.tabIndex = -1; // do not allow focus or typing directly, once tagified\n }\n\n // fixes tagify nested inside a