git-tfs-id: [http://tfs.userrendszerhaz.hu:8080/tfs/DefaultCollection]$/MediaCube...
authorSweidan Omar <TFS\sweidan.omar>
Tue, 22 Feb 2022 15:13:36 +0000 (15:13 +0000)
committerSweidan Omar <TFS\sweidan.omar>
Tue, 22 Feb 2022 15:13:36 +0000 (15:13 +0000)
server/user.mediacube.gui/js/tagify.js
server/user.mediacube.gui/js/tagify.min.js

index 04ab47808d481ca1f104510784ec9e997baa4c61..9382b9e4a6c5ac6efc2f54aac58511e1a2f59c92 100644 (file)
 /**
- * Tagify (v 3.2.6)- tags input component
+ * Tagify (v 4.9.8) - tags input component
  * By Yair Even-Or
  * Don't sell this code. (c)
  * https://github.com/yairEO/tagify
  */
-;(function(root, factory) {
-  if (typeof define === 'function' && define.amd) {
-    define([], factory);
-  } else if (typeof exports === 'object') {
-    module.exports = factory();
-  } else {
-    root.Tagify = factory();
+
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tagify = factory());
+}(this, (function () { 'use strict';
+
+  function ownKeys(object, enumerableOnly) {
+    var keys = Object.keys(object);
+
+    if (Object.getOwnPropertySymbols) {
+      var symbols = Object.getOwnPropertySymbols(object);
+
+      if (enumerableOnly) {
+        symbols = symbols.filter(function (sym) {
+          return Object.getOwnPropertyDescriptor(object, sym).enumerable;
+        });
+      }
+
+      keys.push.apply(keys, symbols);
+    }
+
+    return keys;
   }
-}(this, function() {
-"use strict";
 
-function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
+  function _objectSpread2(target) {
+    for (var i = 1; i < arguments.length; i++) {
+      var source = arguments[i] != null ? arguments[i] : {};
+
+      if (i % 2) {
+        ownKeys(Object(source), true).forEach(function (key) {
+          _defineProperty(target, key, source[key]);
+        });
+      } else if (Object.getOwnPropertyDescriptors) {
+        Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
+      } else {
+        ownKeys(Object(source)).forEach(function (key) {
+          Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
+        });
+      }
+    }
+
+    return target;
+  }
 
-function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
+  function _defineProperty(obj, key, value) {
+    if (key in obj) {
+      Object.defineProperty(obj, key, {
+        value: value,
+        enumerable: true,
+        configurable: true,
+        writable: true
+      });
+    } else {
+      obj[key] = value;
+    }
 
-function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
+    return obj;
+  }
 
-function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
+  // console.json = console.json || function(argument){
+  //     for(var arg=0; arg < arguments.length; ++arg)
+  //         console.log(  JSON.stringify(arguments[arg], null, 4)  )
+  // }
+  // const isEdge = /Edge/.test(navigator.userAgent)
+  const sameStr = (s1, s2, caseSensitive, trim) => {
+    // cast to String
+    s1 = "" + s1;
+    s2 = "" + s2;
+
+    if (trim) {
+      s1 = s1.trim();
+      s2 = s2.trim();
+    }
 
-function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
+    return caseSensitive ? s1 == s2 : s1.toLowerCase() == s2.toLowerCase();
+  }; // const getUID = () => (new Date().getTime() + Math.floor((Math.random()*10000)+1)).toString(16)
 
-function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
+  const removeCollectionProp = (collection, unwantedProps) => collection && Array.isArray(collection) && collection.map(v => omit(v, unwantedProps));
+  function omit(obj, props) {
+    var newObj = {},
+        p;
 
-function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+    for (p in obj) if (props.indexOf(p) < 0) newObj[p] = obj[p];
 
-/**
- * @constructor
- * @param {Object} input    DOM element
- * @param {Object} settings settings object
- */
-function Tagify(input, settings) {
-  // protection
-  if (!input) {
-    console.warn('Tagify: ', 'invalid input element ', input);
-    return this;
+    return newObj;
+  }
+  function decode(s) {
+    var el = document.createElement('div');
+    return s.replace(/\&#?[0-9a-z]+;/gi, function (enc) {
+      el.innerHTML = enc;
+      return el.innerText;
+    });
+  }
+  /**
+   * utility method
+   * https://stackoverflow.com/a/35385518/104380
+   * @param  {String} s [HTML string]
+   * @return {Object}   [DOM node]
+   */
+
+  function parseHTML(s) {
+    var parser = new DOMParser(),
+        node = parser.parseFromString(s.trim(), "text/html");
+    return node.body.firstElementChild;
   }
+  /**
+   * Removed new lines and irrelevant spaces which might affect layout, and are better gone
+   * @param {string} s [HTML string]
+   */
 
-  this.applySettings(input, settings || {});
-  this.state = {
-    editing: {},
-    actions: {},
-    // UI actions for state-locking
-    dropdown: {}
-  };
-  this.value = []; // tags' data
-  // events' callbacks references will be stores here, so events could be unbinded
-
-  this.listeners = {};
-  this.DOM = {}; // Store all relevant DOM elements in an Object
-
-  this.extend(this, new this.EventDispatcher(this));
-  this.build(input);
-  this.getCSSVars();
-  this.loadOriginalValues();
-  this.events.customBinding.call(this);
-  this.events.binding.call(this);
-  input.autofocus && this.DOM.input.focus();
-}
-
-Tagify.prototype = {
-  isIE: window.document.documentMode,
-  // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility
-  TEXTS: {
-    empty: "empty",
-    exceed: "number of tags exceeded",
-    pattern: "pattern mismatch",
-    duplicate: "already exists",
-    notAllowed: "not allowed"
-  },
-  DEFAULTS: {
+  function minify(s) {
+    return s ? s.replace(/\>[\r\n ]+\</g, "><").replace(/(<.*?>)|\s+/g, (m, $1) => $1 ? $1 : ' ') // https://stackoverflow.com/a/44841484/104380
+    : "";
+  }
+  function removeTextChildNodes(elm) {
+    var iter = document.createNodeIterator(elm, NodeFilter.SHOW_TEXT, null, false),
+        textnode; // print all text nodes
+
+    while (textnode = iter.nextNode()) {
+      if (!textnode.textContent.trim()) textnode.parentNode.removeChild(textnode);
+    }
+  }
+  function getfirstTextNode(elm, action) {
+    action = action || 'previous';
+
+    while (elm = elm[action + 'Sibling']) if (elm.nodeType == 3) return elm;
+  }
+  /**
+   * utility method
+   * https://stackoverflow.com/a/6234804/104380
+   */
+
+  function escapeHTML(s) {
+    return typeof s == 'string' ? s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/`|'/g, "&#039;") : s;
+  }
+  /**
+   * Checks if an argument is a javascript Object
+   */
+
+  function isObject(obj) {
+    var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1);
+    return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement';
+  }
+  /**
+   * merge objects into a single new one
+   * TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
+   */
+
+  function extend(o, o1, o2) {
+    if (!(o instanceof Object)) o = {};
+    copy(o, o1);
+    if (o2) copy(o, o2);
+
+    function copy(a, b) {
+      // copy o2 to o
+      for (var key in b) if (b.hasOwnProperty(key)) {
+        if (isObject(b[key])) {
+          if (!isObject(a[key])) a[key] = Object.assign({}, b[key]);else copy(a[key], b[key]);
+          continue;
+        }
+
+        if (Array.isArray(b[key])) {
+          a[key] = Object.assign([], b[key]);
+          continue;
+        }
+
+        a[key] = b[key];
+      }
+    }
+
+    return o;
+  }
+  /**
+   * concatenates N arrays without dups.
+   * If an array's item is an Object, compare by `value`
+   */
+
+  function concatWithoutDups() {
+    const newArr = [],
+          existingObj = {};
+
+    for (let arr of arguments) {
+      for (let item of arr) {
+        // if current item is an object which has yet to be added to the new array
+        if (isObject(item)) {
+          if (!existingObj[item.value]) {
+            newArr.push(item);
+            existingObj[item.value] = 1;
+          }
+        } // if current item is not an object and is not in the new array
+        else if (!newArr.includes(item)) newArr.push(item);
+      }
+    }
+
+    return newArr;
+  }
+  /**
+   *  Extracted from: https://stackoverflow.com/a/37511463/104380
+   * @param {String} s
+   */
+
+  function unaccent(s) {
+    // if not supported, do not continue.
+    // developers should use a polyfill:
+    // https://github.com/walling/unorm
+    if (!String.prototype.normalize) return s;
+    if (typeof s === 'string') return s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+  }
+  /**
+   * Meassures an element's height, which might yet have been added DOM
+   * https://stackoverflow.com/q/5944038/104380
+   * @param {DOM} node
+   */
+
+  function getNodeHeight(node) {
+    var height,
+        clone = node.cloneNode(true);
+    clone.style.cssText = "position:fixed; top:-9999px; opacity:0";
+    document.body.appendChild(clone);
+    height = clone.clientHeight;
+    clone.parentNode.removeChild(clone);
+    return height;
+  }
+  var isChromeAndroidBrowser = () => /(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);
+  function getUID() {
+    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
+  }
+  function isNodeTag(node) {
+    return node && node.classList && node.classList.contains(this.settings.classNames.tag);
+  }
+
+  var DEFAULTS = {
     delimiters: ",",
     // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |."
     pattern: null,
     // RegEx pattern to validate input by. Ex: /[1-9]/
+    tagTextProp: 'value',
+    // tag data Object property which will be displayed as the tag's text
     maxTags: Infinity,
     // Maximum number of tags
     callbacks: {},
@@ -85,377 +250,910 @@ Tagify.prototype = {
     addTagOnBlur: true,
     // Flag - automatically adds the text which was inputed as a tag when blur event happens
     duplicates: false,
-    // Flag - allow tuplicate tags
+    // "true" - allow duplicate tags
     whitelist: [],
     // Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting)
     blacklist: [],
     // A list of non-allowed tags
     enforceWhitelist: false,
-    // Flag - Only allow tags allowed in whitelist
+    // Flag - Only allow tags from the whitelist
+    userInput: true,
+    // Flag - disable manually typing/pasting/editing tags (tags may only be added from the whitelist)
     keepInvalidTags: false,
     // Flag - if true, do not remove tags which did not pass validation
     mixTagsAllowedAfter: /,|\.|\:|\s/,
-    // RegEx - Define conditions in which mix-tags content is allowing a tag to be added after
+    // RegEx - Define conditions in which mix-tags content allows a tag to be added after
     mixTagsInterpolator: ['[[', ']]'],
     // Interpolation for mix mode. Everything between this will becmoe a tag
     backspace: true,
     // false / true / "edit"
     skipInvalid: false,
     // If `true`, do not add invalid, temporary, tags before automatically removing them
-    editTags: 2,
+    pasteAsTags: true,
+    // automatically converts pasted text into tags. if "false", allows for further text editing
+    editTags: {
+      clicks: 2,
+      // clicks to enter "edit-mode": 1 for single click. any other value is considered as double-click
+      keepInvalid: true // keeps invalid edits as-is until `esc` is pressed while in focus
+
+    },
     // 1 or 2 clicks to edit a tag. false/null for not allowing editing
-    transformTag: function transformTag() {},
+    transformTag: () => {},
     // Takes a tag input string as argument and returns a transformed value
+    trim: true,
+    // whether or not the value provided should be trimmed, before being added as a tag
+    a11y: {
+      focusableTags: false
+    },
+    mixMode: {
+      insertAfterTag: '\u00A0' // String/Node to inject after a tag has been added (see #588)
+
+    },
     autoComplete: {
       enabled: true,
       // Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text
       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"
 
     },
+    classNames: {
+      namespace: 'tagify',
+      mixMode: 'tagify--mix',
+      selectMode: 'tagify--select',
+      input: 'tagify__input',
+      focus: 'tagify--focus',
+      tagNoAnimation: 'tagify--noAnim',
+      tagInvalid: 'tagify--invalid',
+      tagNotAllowed: 'tagify--notAllowed',
+      scopeLoading: 'tagify--loading',
+      hasMaxTags: 'tagify--hasMaxTags',
+      hasNoTags: 'tagify--noTags',
+      empty: 'tagify--empty',
+      inputInvalid: 'tagify__input--invalid',
+      dropdown: 'tagify__dropdown',
+      dropdownWrapper: 'tagify__dropdown__wrapper',
+      dropdownItem: 'tagify__dropdown__item',
+      dropdownItemActive: 'tagify__dropdown__item--active',
+      dropdownInital: 'tagify__dropdown--initial',
+      tag: 'tagify__tag',
+      tagText: 'tagify__tag-text',
+      tagX: 'tagify__tag__removeBtn',
+      tagLoading: 'tagify__tag--loading',
+      tagEditing: 'tagify__tag--editable',
+      tagFlash: 'tagify__tag--flash',
+      tagHide: 'tagify__tag--hide'
+    },
     dropdown: {
       classname: '',
       enabled: 2,
-      // minimum input characters needs to be typed for the dropdown to show
+      // minimum input characters to be typed for the suggestions dropdown to show
       maxItems: 10,
-      searchKeys: [],
+      searchKeys: ["value", "searchBy"],
       fuzzySearch: true,
+      caseSensitive: false,
+      accentedSearch: true,
       highlightFirst: false,
       // highlights first-matched item in the list
       closeOnSelect: true,
       // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown)
-      position: 'all' // 'manual' / 'text' / 'all'
+      clearOnSelect: true,
+      // after selecting a suggetion, should the typed text input remain or be cleared
+      position: 'all',
+      // 'manual' / 'text' / 'all'
+      appendTarget: null // defaults to document.body one DOM has been loaded
 
-    }
-  },
-  // Using ARIA & role attributes
-  // https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
-  templates: {
-    wrapper: function wrapper(input, settings) {
-      return "<tags class=\"tagify ".concat(settings.mode ? "tagify--" + settings.mode : "", " ").concat(input.className, "\"\n                        ").concat(settings.readonly ? 'readonly aria-readonly="true"' : 'aria-haspopup="listbox" aria-expanded="false"', "\n                        role=\"tagslist\"\n                        tabIndex=\"-1\">\n                <span contenteditable data-placeholder=\"").concat(settings.placeholder || '&#8203;', "\" aria-placeholder=\"").concat(settings.placeholder || '', "\"\n                    class=\"tagify__input\"\n                    role=\"textbox\"\n                    aria-controls=\"dropdown\"\n                    aria-autocomplete=\"both\"\n                    aria-multiline=\"").concat(settings.mode == 'mix' ? true : false, "\"></span>\n            </tags>");
-    },
-    tag: function tag(value, tagData) {
-      return "<tag title='".concat(tagData.title || value, "'\n                        contenteditable='false'\n                        spellcheck='false'\n                        tabIndex=\"-1\"\n                        class='tagify__tag ").concat(tagData["class"] ? tagData["class"] : "", "'\n                        ").concat(this.getAttributes(tagData), ">\n                <x title='' class='tagify__tag__removeBtn' role='button' aria-label='remove tag'></x>\n                <div>\n                    <span class='tagify__tag-text'>").concat(value, "</span>\n                </div>\n            </tag>");
     },
-    dropdownItem: function dropdownItem(item) {
-      var mapValueTo = this.settings.dropdown.mapValueTo,
-          value = (mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(item) : item[mapValueTo] : item.value) || item.value,
-          sanitizedValue = (value || item).replace(/`|'/g, "&#39;");
-      return "<div ".concat(this.getAttributes(item), "\n                        class='tagify__dropdown__item ").concat(item["class"] ? item["class"] : "", "'\n                        tabindex=\"0\"\n                        role=\"option\"\n                        aria-labelledby=\"dropdown-label\">").concat(sanitizedValue, "</div>");
+    hooks: {
+      beforeRemoveTag: () => Promise.resolve(),
+      beforePaste: () => Promise.resolve(),
+      suggestionClick: () => Promise.resolve()
     }
-  },
-  customEventsList: ['add', 'remove', 'invalid', 'input', 'click', 'keydown', 'focus', 'blur', 'edit:input', 'edit:updated', 'edit:start', 'edit:keydown', 'dropdown:show', 'dropdown:hide', 'dropdown:select'],
-  applySettings: function applySettings(input, settings) {
-    var _this2 = this;
-
-    this.DEFAULTS.templates = this.templates;
-    this.settings = this.extend({}, this.DEFAULTS, settings);
-    this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component
-
-    this.settings.placeholder = input.getAttribute('placeholder') || this.settings.placeholder || "";
-    if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
-
-    ["whitelist", "blacklist"].forEach(function (name) {
-      var attrVal = input.getAttribute('data-' + name);
-
-      if (attrVal) {
-        attrVal = attrVal.split(_this2.settings.delimiters);
-        if (attrVal instanceof Array) _this2.settings[name] = attrVal;
-      }
-    }); // backward-compatibility for old version of "autoComplete" setting:
+  };
 
-    if ("autoComplete" in settings && !this.isObject(settings.autoComplete)) {
-      this.settings.autoComplete = this.DEFAULTS.autoComplete;
-      this.settings.autoComplete.enabled = settings.autoComplete;
-    }
+  function initDropdown() {
+    this.dropdown = {};
 
-    if (input.pattern) try {
-      this.settings.pattern = new RegExp(input.pattern);
-    } catch (e) {} // Convert the "delimiters" setting into a REGEX object
+    for (let p in this._dropdown) this.dropdown[p] = typeof this._dropdown[p] === 'function' ? this._dropdown[p].bind(this) : this._dropdown[p];
 
-    if (this.settings.delimiters) {
-      try {
-        this.settings.delimiters = new RegExp(this.settings.delimiters, "g");
-      } catch (e) {}
-    } // make sure the dropdown will be shown on "focus" and not only after typing something (in "select" mode)
+    this.dropdown.refs();
+  }
+  var _dropdown = {
+    refs() {
+      this.DOM.dropdown = this.parseTemplate('dropdown', [this.settings]);
+      this.DOM.dropdown.content = this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector);
+    },
 
+    /**
+     * shows the suggestions select box
+     * @param {String} value [optional, filter the whitelist by this value]
+     */
+    show(value) {
+      var _s = this.settings,
+          firstListItem,
+          firstListItemValue,
+          allowNewTags = _s.mode == 'mix' && !_s.enforceWhitelist,
+          noWhitelist = !_s.whitelist || !_s.whitelist.length,
+          noMatchListItem,
+          isManual = _s.dropdown.position == 'manual'; // if text still exists in the input, and `show` method has no argument, then the input's text should be used
+
+      value = value === undefined ? this.state.inputText : value; // ⚠️ Do not render suggestions list  if:
+      // 1. there's no whitelist (can happen while async loading) AND new tags arn't allowed
+      // 2. dropdown is disabled
+      // 3. loader is showing (controlled outside of this code)
+
+      if (noWhitelist && !allowNewTags && !_s.templates.dropdownItemNoMatch || _s.dropdown.enable === false || this.state.isLoading) return;
+      clearTimeout(this.dropdownHide__bindEventsTimeout); // if no value was supplied, show all the "whitelist" items in the dropdown
+      // @type [Array] listItems
+      // TODO: add a Setting to control items' sort order for "listItems"
 
-    if (this.settings.mode == 'select') this.settings.dropdown.enabled = 0;
-    if (this.settings.mode == 'mix') this.settings.autoComplete.rightKey = true;
-  },
+      this.suggestedListItems = this.dropdown.filterListItems(value); // trigger at this exact point to let the developer the chance to manually set "this.suggestedListItems"
 
-  /**
-   * Creates a string of HTML element attributes
-   * @param {Object} data [Tag data]
-   */
-  getAttributes: function getAttributes(data) {
-    // only items which are objects have properties which can be used as attributes
-    if (Object.prototype.toString.call(data) != "[object Object]") return '';
-    var keys = Object.keys(data),
-        s = "",
-        propName,
-        i;
-
-    for (i = keys.length; i--;) {
-      propName = keys[i];
-      if (propName != 'class' && data.hasOwnProperty(propName) && data[propName]) s += " " + propName + (data[propName] ? "=\"".concat(data[propName], "\"") : "");
-    }
+      if (value && !this.suggestedListItems.length) {
+        this.trigger('dropdown:noMatch', value);
+        if (_s.templates.dropdownItemNoMatch) noMatchListItem = _s.templates.dropdownItemNoMatch.call(this, {
+          value
+        });
+      } // if "dropdownItemNoMatch" was no defined, procceed regular flow.
+      //
 
-    return s;
-  },
 
-  /**
-   * utility method
-   * https://stackoverflow.com/a/35385518/104380
-   * @param  {String} s [HTML string]
-   * @return {Object}   [DOM node]
-   */
-  parseHTML: function parseHTML(s) {
-    var parser = new DOMParser(),
-        node = parser.parseFromString(s.trim(), "text/html");
-    return node.body.firstElementChild;
-  },
+      if (!noMatchListItem) {
+        // in mix-mode, if the value isn't included in the whilelist & "enforceWhitelist" setting is "false",
+        // then add a custom suggestion item to the dropdown
+        if (this.suggestedListItems.length) {
+          if (value && allowNewTags && !this.state.editing.scope && !sameStr(this.suggestedListItems[0].value, value)) this.suggestedListItems.unshift({
+            value
+          });
+        } else {
+          if (value && allowNewTags && !this.state.editing.scope) {
+            this.suggestedListItems = [{
+              value
+            }];
+          } // hide suggestions list if no suggestion matched
+          else {
+            this.input.autocomplete.suggest.call(this);
+            this.dropdown.hide();
+            return;
+          }
+        }
 
-  /**
-   * utility method
-   * https://stackoverflow.com/a/25396011/104380
-   */
-  escapeHTML: function escapeHTML(s) {
-    var text = document.createTextNode(s),
-        p = document.createElement('p');
-    p.appendChild(text);
-    return p.innerHTML;
-  },
+        firstListItem = this.suggestedListItems[0];
+        firstListItemValue = "" + (isObject(firstListItem) ? firstListItem.value : firstListItem);
 
-  /**
-   * Get the caret position relative to the viewport
-   * https://stackoverflow.com/q/58985076/104380
-   *
-   * @returns {object} left, top distance in pixels
-   */
-  getCaretGlobalPosition: function getCaretGlobalPosition() {
-    var sel = document.getSelection();
-
-    if (sel.rangeCount) {
-      var r = sel.getRangeAt(0);
-      var node = r.startContainer;
-      var offset = r.startOffset;
-      var rect, r2;
-
-      if (offset > 0) {
-        r2 = document.createRange();
-        r2.setStart(node, offset - 1);
-        r2.setEnd(node, offset);
-        rect = r2.getBoundingClientRect();
-        return {
-          left: rect.right,
-          top: rect.top,
-          bottom: rect.bottom
-        };
+        if (_s.autoComplete && firstListItemValue) {
+          // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of "fuzzysearch" setting)
+          if (firstListItemValue.indexOf(value) == 0) this.input.autocomplete.suggest.call(this, firstListItem);
+        }
       }
-    }
-
-    return {
-      left: -9999,
-      top: -9999
-    };
-  },
-
-  /**
-   * Get specific CSS variables which are relevant to this script and parse them as needed.
-   * The result is saved on the instance in "this.CSSVars"
-   */
-  getCSSVars: function getCSSVars() {
-    var compStyle = getComputedStyle(this.DOM.scope, null);
-
-    var getProp = function getProp(name) {
-      return compStyle.getPropertyValue('--' + name);
-    };
 
-    function seprateUnitFromValue(a) {
-      if (!a) return {};
-      a = a.trim().split(' ')[0];
-      var unit = a.split(/\d+/g).filter(function (n) {
-        return n;
-      }).pop().trim(),
-          value = +a.split(unit).filter(function (n) {
-        return n;
-      })[0].trim();
-      return {
-        value: value,
-        unit: unit
-      };
-    }
+      this.dropdown.fill(noMatchListItem);
+      if (_s.dropdown.highlightFirst) this.dropdown.highlightOption(this.DOM.dropdown.content.children[0]); // bind events, exactly at this stage of the code. "dropdown.show" method is allowed to be
+      // called multiple times, regardless if the dropdown is currently visible, but the events-binding
+      // should only be called if the dropdown wasn't previously visible.
 
-    this.CSSVars = {
-      tagHideTransition: function (_ref) {
-        var value = _ref.value,
-            unit = _ref.unit;
-        return unit == 's' ? value * 1000 : value;
-      }(seprateUnitFromValue(getProp('tag-hide-transition')))
-    };
-  },
+      if (!this.state.dropdown.visible) // timeout is needed for when pressing arrow down to show the dropdown,
+        // so the key event won't get registered in the dropdown events listeners
+        setTimeout(this.dropdown.events.binding.bind(this)); // set the dropdown visible state to be the same as the searched value.
+      // MUST be set *before* position() is called
 
-  /**
-   * builds the HTML of this component
-   * @param  {Object} input [DOM element which would be "transformed" into "Tags"]
-   */
-  build: function build(input) {
-    var DOM = this.DOM,
-        template = this.settings.templates.wrapper(input, this.settings);
-    DOM.originalInput = input;
-    DOM.scope = this.parseHTML(template);
-    DOM.input = DOM.scope.querySelector('[contenteditable]');
-    input.parentNode.insertBefore(DOM.scope, input);
-
-    if (this.settings.dropdown.enabled >= 0) {
-      this.dropdown.init.call(this);
-    }
-  },
+      this.state.dropdown.visible = value || true;
+      this.state.dropdown.query = value;
+      this.setStateSelection(); // try to positioning the dropdown (it might not yet be on the page, doesn't matter, next code handles this)
+
+      if (!isManual) {
+        // a slight delay is needed if the dropdown "position" setting is "text", and nothing was typed in the input,
+        // so sadly the "getCaretGlobalPosition" method doesn't recognize the caret position without this delay
+        setTimeout(() => {
+          this.dropdown.position();
+          this.dropdown.render();
+        });
+      } // a delay is needed because of the previous delay reason.
+      // this event must be fired after the dropdown was rendered & positioned
 
-  /**
-   * revert any changes made by this component
-   */
-  destroy: function destroy() {
-    this.DOM.scope.parentNode.removeChild(this.DOM.scope);
-    this.dropdown.hide.call(this, true);
-  },
 
-  /**
-   * if the original input had any values, add them as tags
-   */
-  loadOriginalValues: function loadOriginalValues(value) {
-    value = value || this.DOM.originalInput.value; // if the original input already had any value (tags)
+      setTimeout(() => {
+        this.trigger("dropdown:show", this.DOM.dropdown);
+      });
+    },
 
-    if (!value) return;
-    this.removeAllTags();
-    if (this.settings.mode == 'mix') this.parseMixTags(value.trim());else {
-      try {
-        if (typeof JSON.parse(value) !== 'string') value = JSON.parse(value);
-      } catch (err) {}
+    /**
+     * Hides the dropdown (if it's not managed manually by the developer)
+     * @param {Boolean} overrideManual
+     */
+    hide(overrideManual) {
+      var _this$DOM = this.DOM,
+          scope = _this$DOM.scope,
+          dropdown = _this$DOM.dropdown,
+          isManual = this.settings.dropdown.position == 'manual' && !overrideManual; // if there's no dropdown, this means the dropdown events aren't binded
 
-      this.addTags(value).forEach(function (tag) {
-        return tag && tag.classList.add('tagify--noAnim');
-      });
-    }
-  },
+      if (!dropdown || !document.body.contains(dropdown) || isManual) return;
+      window.removeEventListener('resize', this.dropdown.position);
+      this.dropdown.events.binding.call(this, false); // unbind all events
+      // if the dropdown is open, and the input (scope) is clicked,
+      // the dropdown should be now "close", and the next click (on the scope)
+      // should re-open it, and without a timeout, clicking to close will re-open immediately
+      //  clearTimeout(this.dropdownHide__bindEventsTimeout)
+      //  this.dropdownHide__bindEventsTimeout = setTimeout(this.events.binding.bind(this), 250)  // re-bind main events
 
-  /**
-   * Checks if an argument is a javascript Object
-   */
-  isObject: function isObject(obj) {
-    var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1);
-    return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement';
-  },
+      scope.setAttribute("aria-expanded", false);
+      dropdown.parentNode.removeChild(dropdown); // scenario: clicking the scope to show the dropdown, clicking again to hide -> calls dropdown.hide() and then re-focuses the input
+      // which casues another onFocus event, which checked "this.state.dropdown.visible" and see it as "false" and re-open the dropdown
 
-  /**
-   * merge objects into a single new one
-   * TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
-   */
-  extend: function extend(o, o1, o2) {
-    var that = this;
-    if (!(o instanceof Object)) o = {};
-    copy(o, o1);
-    if (o2) copy(o, o2);
+      setTimeout(() => {
+        this.state.dropdown.visible = false;
+      }, 100);
+      this.state.dropdown.query = this.state.ddItemData = this.state.ddItemElm = this.state.selection = null; // if the user closed the dropdown (in mix-mode) while a potential tag was detected, flag the current tag
+      // so the dropdown won't be shown on following user input for that "tag"
 
-    function copy(a, b) {
-      // copy o2 to o
-      for (var key in b) {
-        if (b.hasOwnProperty(key)) {
-          if (that.isObject(b[key])) {
-            if (!that.isObject(a[key])) a[key] = Object.assign({}, b[key]);else copy(a[key], b[key]);
-          } else a[key] = b[key];
-        }
+      if (this.state.tag && this.state.tag.value.length) {
+        this.state.flaggedTags[this.state.tag.baseOffset] = this.state.tag;
       }
-    }
 
-    return o;
-  },
-  cloneEvent: function cloneEvent(e) {
-    var clonedEvent = {};
+      this.trigger("dropdown:hide", dropdown);
+      return this;
+    },
 
-    for (var v in e) {
-      clonedEvent[v] = e[v];
-    }
+    /**
+     * Toggles dropdown show/hide
+     * @param {Boolean} show forces the dropdown to show
+     */
+    toggle(show) {
+      this.dropdown[this.state.dropdown.visible && !show ? 'hide' : 'show']();
+    },
 
-    return clonedEvent;
-  },
+    render() {
+      // let the element render in the DOM first, to accurately measure it.
+      // this.DOM.dropdown.style.cssText = "left:-9999px; top:-9999px;";
+      var ddHeight = getNodeHeight(this.DOM.dropdown),
+          _s = this.settings,
+          enabled = typeof _s.dropdown.enabled == 'number' && _s.dropdown.enabled >= 0;
+      if (!enabled) return this;
+      this.DOM.scope.setAttribute("aria-expanded", true); // if the dropdown has yet to be appended to the DOM,
+      // append the dropdown to the body element & handle events
 
-  /**
-   * A constructor for exposing events to the outside
-   */
-  EventDispatcher: function EventDispatcher(instance) {
-    // Create a DOM EventTarget object
-    var target = document.createTextNode('');
+      if (!document.body.contains(this.DOM.dropdown)) {
+        this.DOM.dropdown.classList.add(_s.classNames.dropdownInital);
+        this.dropdown.position(ddHeight);
 
-    function addRemove(op, events, cb) {
-      if (cb) events.split(/\s+/g).forEach(function (name) {
-        return target[op + 'EventListener'].call(target, name, cb);
-      });
-    } // Pass EventTarget interface calls to DOM EventTarget object
+        _s.dropdown.appendTarget.appendChild(this.DOM.dropdown);
 
+        setTimeout(() => this.DOM.dropdown.classList.remove(_s.classNames.dropdownInital));
+      }
 
-    this.off = function (events, cb) {
-      addRemove('remove', events, cb);
       return this;
-    };
+    },
 
-    this.on = function (events, cb) {
-      if (cb && typeof cb == 'function') addRemove('add', events, cb);
-      return this;
-    };
+    /**
+     *
+     * @param {String/Array} HTMLContent - optional
+     */
+    fill(HTMLContent) {
+      HTMLContent = typeof HTMLContent == 'string' ? HTMLContent : this.dropdown.createListHTML(HTMLContent || this.suggestedListItems);
+      this.DOM.dropdown.content.innerHTML = minify(HTMLContent);
+    },
 
-    this.trigger = function (eventName, data) {
-      var e;
-      if (!eventName) return;
+    /**
+     * fill data into the suggestions list
+     * (mainly used to update the list when removing tags, so they will be re-added to the list. not efficient)
+     */
+    refilter(value) {
+      value = value || this.state.dropdown.query || '';
+      this.suggestedListItems = this.dropdown.filterListItems(value);
+      this.dropdown.fill();
+      if (!this.suggestedListItems.length) this.dropdown.hide();
+      this.trigger("dropdown:updated", this.DOM.dropdown);
+    },
 
-      if (instance.settings.isJQueryPlugin) {
-        if (eventName == 'remove') eventName = 'removeTag'; // issue #222
+    position(ddHeight) {
+      var _sd = this.settings.dropdown;
+      if (_sd.position == 'manual') return;
+      var rect,
+          top,
+          bottom,
+          left,
+          width,
+          parentsPositions,
+          ddElm = this.DOM.dropdown,
+          placeAbove = _sd.placeAbove,
+          viewportHeight = document.documentElement.clientHeight,
+          viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
+          positionTo = viewportWidth > 480 ? _sd.position : 'all',
+          ddTarget = this.DOM[positionTo == 'input' ? 'input' : 'scope'];
+      ddHeight = ddHeight || ddElm.clientHeight;
+
+      function getParentsPositions(p) {
+        var left = 0,
+            top = 0;
+
+        while (p) {
+          left += p.offsetLeft || 0;
+          top += p.offsetTop || 0;
+          p = p.parentNode;
+        }
+
+        return {
+          left,
+          top
+        };
+      }
+
+      if (!this.state.dropdown.visible) return;
 
-        jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data]);
+      if (positionTo == 'text') {
+        rect = this.getCaretGlobalPosition();
+        bottom = rect.bottom;
+        top = rect.top;
+        left = rect.left;
+        width = 'auto';
       } else {
-        try {
-          e = new CustomEvent(eventName, {
-            "detail": this.extend({}, data, {
-              tagify: this
-            })
+        parentsPositions = getParentsPositions(this.settings.dropdown.appendTarget);
+        rect = ddTarget.getBoundingClientRect();
+        top = rect.top - parentsPositions.top;
+        bottom = rect.bottom - 1 - parentsPositions.top;
+        left = rect.left - parentsPositions.left;
+        width = rect.width + 'px';
+      }
+
+      top = Math.floor(top);
+      bottom = Math.ceil(bottom);
+      placeAbove = placeAbove === undefined ? viewportHeight - rect.bottom < ddHeight : placeAbove; // flip vertically if there is no space for the dropdown below the input
+
+      ddElm.style.cssText = "left:" + (left + window.pageXOffset) + "px; width:" + width + ";" + (placeAbove ? "top: " + (top + window.pageYOffset) + "px" : "top: " + (bottom + window.pageYOffset) + "px");
+      ddElm.setAttribute('placement', placeAbove ? "top" : "bottom");
+      ddElm.setAttribute('position', positionTo);
+    },
+
+    events: {
+      /**
+       * Events should only be binded when the dropdown is rendered and removed when isn't
+       * because there might be multiple Tagify instances on a certain page
+       * @param  {Boolean} bindUnbind [optional. true when wanting to unbind all the events]
+       */
+      binding(bindUnbind = true) {
+        // references to the ".bind()" methods must be saved so they could be unbinded later
+        var _CB = this.dropdown.events.callbacks,
+            // callback-refs
+        _CBR = this.listeners.dropdown = this.listeners.dropdown || {
+          position: this.dropdown.position.bind(this),
+          onKeyDown: _CB.onKeyDown.bind(this),
+          onMouseOver: _CB.onMouseOver.bind(this),
+          onMouseLeave: _CB.onMouseLeave.bind(this),
+          onClick: _CB.onClick.bind(this),
+          onScroll: _CB.onScroll.bind(this)
+        },
+            action = bindUnbind ? 'addEventListener' : 'removeEventListener';
+
+        if (this.settings.dropdown.position != 'manual') {
+          window[action]('resize', _CBR.position);
+          window[action]('keydown', _CBR.onKeyDown);
+        }
+
+        this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver);
+        this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave);
+        this.DOM.dropdown[action]('mousedown', _CBR.onClick);
+        this.DOM.dropdown.content[action]('scroll', _CBR.onScroll);
+      },
+
+      callbacks: {
+        onKeyDown(e) {
+          // get the "active" element, and if there was none (yet) active, use first child
+          var selectedElm = this.DOM.dropdown.querySelector(this.settings.classNames.dropdownItemActiveSelector),
+              selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm);
+
+          switch (e.key) {
+            case 'ArrowDown':
+            case 'ArrowUp':
+            case 'Down': // >IE11
+
+            case 'Up':
+              {
+                // >IE11
+                e.preventDefault();
+                var dropdownItems;
+                if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop
+
+                if (!selectedElm) {
+                  dropdownItems = this.DOM.dropdown.content.children;
+                  selectedElm = dropdownItems[e.key == 'ArrowUp' || e.key == 'Up' ? dropdownItems.length - 1 : 0];
+                }
+
+                selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm);
+                this.dropdown.highlightOption(selectedElm, true);
+                break;
+              }
+
+            case 'Escape':
+            case 'Esc':
+              // IE11
+              this.dropdown.hide();
+              break;
+
+            case 'ArrowRight':
+              if (this.state.actions.ArrowLeft) return;
+
+            case 'Tab':
+              {
+                // in mix-mode, treat arrowRight like Enter key, so a tag will be created
+                if (this.settings.mode != 'mix' && selectedElm && !this.settings.autoComplete.rightKey && !this.state.editing) {
+                  e.preventDefault(); // prevents blur so the autocomplete suggestion will not become a tag
+
+                  var value = this.dropdown.getMappedValue(selectedElmData);
+                  this.input.autocomplete.set.call(this, value);
+                  return false;
+                }
+
+                return true;
+              }
+
+            case 'Enter':
+              {
+                e.preventDefault();
+                this.settings.hooks.suggestionClick(e, {
+                  tagify: this,
+                  tagData: selectedElmData,
+                  suggestionElm: selectedElm
+                }).then(() => {
+                  if (selectedElm) return this.dropdown.selectOption(selectedElm);else this.dropdown.hide();
+                  if (this.settings.mode != 'mix') this.addTags(this.state.inputText.trim(), true);
+                }).catch(err => err);
+                break;
+              }
+
+            case 'Backspace':
+              {
+                if (this.settings.mode == 'mix' || this.state.editing.scope) return;
+                const value = this.input.raw.call(this);
+
+                if (value == "" || value.charCodeAt(0) == 8203) {
+                  if (this.settings.backspace === true) this.removeTags();else if (this.settings.backspace == 'edit') setTimeout(this.editTag.bind(this), 0);
+                }
+              }
+          }
+        },
+
+        onMouseOver(e) {
+          var ddItem = e.target.closest(this.settings.classNames.dropdownItemSelector); // event delegation check
+
+          ddItem && this.dropdown.highlightOption(ddItem);
+        },
+
+        onMouseLeave(e) {
+          // de-highlight any previously highlighted option
+          this.dropdown.highlightOption();
+        },
+
+        onClick(e) {
+          if (e.button != 0 || e.target == this.DOM.dropdown || e.target == this.DOM.dropdown.content) return; // allow only mouse left-clicks
+
+          var selectedElm = e.target.closest(this.settings.classNames.dropdownItemSelector),
+              selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm); // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run
+
+          this.state.actions.selectOption = true;
+          setTimeout(() => this.state.actions.selectOption = false, 50);
+          this.settings.hooks.suggestionClick(e, {
+            tagify: this,
+            tagData: selectedElmData,
+            suggestionElm: selectedElm
+          }).then(() => {
+            if (selectedElm) this.dropdown.selectOption(selectedElm);else this.dropdown.hide();
+          }).catch(err => console.warn(err));
+        },
+
+        onScroll(e) {
+          var elm = e.target,
+              pos = elm.scrollTop / (elm.scrollHeight - elm.parentNode.clientHeight) * 100;
+          this.trigger("dropdown:scroll", {
+            percentage: Math.round(pos)
+          });
+        }
+
+      }
+    },
+
+    getSuggestionDataByNode(tagElm) {
+      var idx = tagElm ? +tagElm.getAttribute('tagifySuggestionIdx') : -1;
+      return this.suggestedListItems[idx] || null;
+    },
+
+    /**
+     * mark the currently active suggestion option
+     * @param {Object}  elm            option DOM node
+     * @param {Boolean} adjustScroll   when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element
+     */
+    highlightOption(elm, adjustScroll) {
+      var className = this.settings.classNames.dropdownItemActive,
+          itemData; // focus casues a bug in Firefox with the placeholder been shown on the input element
+      // if( this.settings.dropdown.position != 'manual' )
+      //     elm.focus();
+
+      if (this.state.ddItemElm) {
+        this.state.ddItemElm.classList.remove(className);
+        this.state.ddItemElm.removeAttribute("aria-selected");
+      }
+
+      if (!elm) {
+        this.state.ddItemData = null;
+        this.state.ddItemElm = null;
+        this.input.autocomplete.suggest.call(this);
+        return;
+      }
+
+      itemData = this.suggestedListItems[this.getNodeIndex(elm)];
+      this.state.ddItemData = itemData;
+      this.state.ddItemElm = elm; // this.DOM.dropdown.querySelectorAll("." + this.settings.classNames.dropdownItemActive).forEach(activeElm => activeElm.classList.remove(className));
+
+      elm.classList.add(className);
+      elm.setAttribute("aria-selected", true);
+      if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; // Try to autocomplete the typed value with the currently highlighted dropdown item
+
+      if (this.settings.autoComplete) {
+        this.input.autocomplete.suggest.call(this, itemData);
+        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
+      }
+    },
+
+    /**
+     * Create a tag from the currently active suggestion option
+     * @param {Object} elm  DOM node to select
+     */
+    selectOption(elm) {
+      var _this$settings$dropdo = this.settings.dropdown,
+          clearOnSelect = _this$settings$dropdo.clearOnSelect,
+          closeOnSelect = _this$settings$dropdo.closeOnSelect;
+
+      if (!elm) {
+        this.addTags(this.state.inputText, true);
+        closeOnSelect && this.dropdown.hide();
+        return;
+      } // if in edit-mode, do not continue but instead replace the tag's text.
+      // the scenario is that "addTags" was called from a dropdown suggested option selected while editing
+
+
+      var tagifySuggestionIdx = elm.getAttribute('tagifySuggestionIdx'),
+          tagData = this.suggestedListItems[+tagifySuggestionIdx];
+      this.trigger("dropdown:select", {
+        data: tagData,
+        elm
+      }); // The above event must be triggered, regardless of anything else which might go wrong
+
+      if (!tagifySuggestionIdx || !tagData) {
+        this.dropdown.hide();
+        return;
+      }
+
+      if (this.state.editing) {
+        // normalizing value, because "tagData" might be a string, and therefore will not be able to extend the object
+        this.onEditTagDone(null, extend({
+          __isValid: true
+        }, this.normalizeTags([tagData])[0]));
+      } // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing
+      else {
+        this[this.settings.mode == 'mix' ? "addMixTags" : "addTags"]([tagData], clearOnSelect);
+      } // todo: consider not doing this on mix-mode
+
+
+      if (!this.DOM.input.parentNode) return;
+      setTimeout(() => {
+        this.DOM.input.focus();
+        this.toggleFocusClass(true);
+      });
+
+      if (closeOnSelect) {
+        setTimeout(this.dropdown.hide.bind(this));
+      } else this.dropdown.refilter();
+    },
+
+    selectAll() {
+      // having suggestedListItems with items messes with "normalizeTags" when wanting
+      // to add all tags
+      this.suggestedListItems.length = 0;
+      this.dropdown.hide(); // some whitelist items might have already been added as tags so when addings all of them,
+      // skip adding already-added ones, so best to use "filterListItems" method over "settings.whitelist"
+
+      this.addTags(this.dropdown.filterListItems(''), true);
+      return this;
+    },
+
+    /**
+     * returns an HTML string of the suggestions' list items
+     * @param {String} value string to filter the whitelist by
+     * @param {Object} options "exact" - for exact complete match
+     * @return {Array} list of filtered whitelist items according to the settings provided and current value
+     */
+    filterListItems(value, options) {
+      var _s = this.settings,
+          _sd = _s.dropdown,
+          options = options || {},
+          value = _s.mode == 'select' && this.value.length && this.value[0][_s.tagTextProp] == value ? '' // do not filter if the tag, which is already selecetd in "select" mode, is the same as the typed text
+      : value,
+          list = [],
+          exactMatchesList = [],
+          whitelist = _s.whitelist,
+          suggestionsCount = _sd.maxItems || Infinity,
+          searchKeys = _sd.searchKeys,
+          whitelistItem,
+          valueIsInWhitelist,
+          searchBy,
+          isDuplicate,
+          niddle,
+          i = 0;
+
+      if (!value || !searchKeys.length) {
+        return (_s.duplicates ? whitelist : whitelist.filter(item => !this.isTagDuplicate(isObject(item) ? item.value : item)) // don't include tags which have already been added.
+        ).slice(0, suggestionsCount); // respect "maxItems" dropdown setting
+      }
+
+      niddle = _sd.caseSensitive ? "" + value : ("" + value).toLowerCase(); // checks if ALL of the words in the search query exists in the current whitelist item, regardless of their order
+
+      function stringHasAll(s, query) {
+        return query.toLowerCase().split(' ').every(q => s.includes(q.toLowerCase()));
+      }
+
+      for (; i < whitelist.length; i++) {
+        let startsWithMatch, exactMatch;
+        whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : {
+          value: whitelist[i]
+        }; //normalize value as an Object
+
+        let itemWithoutSearchKeys = !Object.keys(whitelistItem).some(k => searchKeys.includes(k)),
+            _searchKeys = itemWithoutSearchKeys ? ["value"] : searchKeys;
+
+        if (_sd.fuzzySearch && !options.exact) {
+          searchBy = _searchKeys.reduce((values, k) => values + " " + (whitelistItem[k] || ""), "").toLowerCase().trim();
+
+          if (_sd.accentedSearch) {
+            searchBy = unaccent(searchBy);
+            niddle = unaccent(niddle);
+          }
+
+          startsWithMatch = searchBy.indexOf(niddle) == 0;
+          exactMatch = searchBy === niddle;
+          valueIsInWhitelist = stringHasAll(searchBy, niddle);
+        } else {
+          startsWithMatch = true;
+          valueIsInWhitelist = _searchKeys.some(k => {
+            var v = '' + (whitelistItem[k] || ''); // if key exists, cast to type String
+
+            if (_sd.accentedSearch) {
+              v = unaccent(v);
+              niddle = unaccent(niddle);
+            }
+
+            if (!_sd.caseSensitive) v = v.toLowerCase();
+            exactMatch = v === niddle;
+            return options.exact ? v === niddle : v.indexOf(niddle) == 0;
           });
-        } catch (err) {
-          console.warn(err);
         }
 
-        target.dispatchEvent(e);
+        isDuplicate = !_s.duplicates && this.isTagDuplicate(isObject(whitelistItem) ? whitelistItem.value : whitelistItem); // match for the value within each "whitelist" item
+
+        if (valueIsInWhitelist && !isDuplicate) if (exactMatch && startsWithMatch) exactMatchesList.push(whitelistItem);else if (_sd.sortby == 'startsWith' && startsWithMatch) list.unshift(whitelistItem);else list.push(whitelistItem);
+      } // custom sorting function
+
+
+      return typeof _sd.sortby == 'function' ? _sd.sortby(exactMatchesList.concat(list), niddle) : exactMatchesList.concat(list).slice(0, suggestionsCount);
+    },
+
+    /**
+     * Returns the final value of a tag data (object) with regards to the "mapValueTo" dropdown setting
+     * @param {Object} tagData
+     * @returns
+     */
+    getMappedValue(tagData) {
+      var mapValueTo = this.settings.dropdown.mapValueTo,
+          value = mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(tagData) : tagData[mapValueTo] || tagData.value : tagData.value;
+      return value;
+    },
+
+    /**
+     * Creates the dropdown items' HTML
+     * @param  {Array} list  [Array of Objects]
+     * @return {String}
+     */
+    createListHTML(optionsArr) {
+      return extend([], optionsArr).map((suggestion, idx) => {
+        if (typeof suggestion == 'string' || typeof suggestion == 'number') suggestion = {
+          value: suggestion
+        };
+        var value = this.dropdown.getMappedValue(suggestion);
+        suggestion.value = value && typeof value == 'string' ? escapeHTML(value) : value;
+        var tagHTMLString = this.settings.templates.dropdownItem.apply(this, [suggestion, this]); // make sure the sugestion index is present as attribute, to match the data when one is selected
+
+        tagHTMLString = tagHTMLString.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gmi, '') // remove the "tagifySuggestionIdx" attribute if for some reason was there
+        .replace('>', ` tagifySuggestionIdx="${idx}">`); // add "tagifySuggestionIdx"
+
+        return tagHTMLString;
+      }).join("");
+    }
+
+  };
+
+  const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped.
+
+  const STORE_KEY = '@yaireo/tagify/';
+  const getPersistedData = id => key => {
+    // if "persist" is "false", do not save to localstorage
+    let customKey = '/' + key,
+        persistedData,
+        versionMatch = localStorage.getItem(STORE_KEY + id + '/v', VERSION) == VERSION;
+
+    if (versionMatch) {
+      try {
+        persistedData = JSON.parse(localStorage[STORE_KEY + id + customKey]);
+      } catch (err) {}
+    }
+
+    return persistedData;
+  };
+  const setPersistedData = id => {
+    if (!id) return () => {}; // for storage invalidation
+
+    localStorage.setItem(STORE_KEY + id + '/v', VERSION);
+    return (data, key) => {
+      let customKey = '/' + key,
+          persistedData = JSON.stringify(data);
+
+      if (data && key) {
+        localStorage.setItem(STORE_KEY + id + customKey, persistedData);
+        dispatchEvent(new Event('storage'));
       }
     };
-  },
+  };
+  const clearPersistedData = id => key => {
+    const base = STORE_KEY + '/' + id + '/'; // delete specific key in the storage
 
-  /**
-   * Toogle loading state on/off
-   * @param {Boolean} isLoading
-   */
-  loading: function loading(isLoading) {
-    // IE11 doesn't support toggle with second parameter
-    this.DOM.scope.classList[isLoading ? "add" : "remove"]('tagify--loading');
-    return this;
-  },
-  toggleFocusClass: function toggleFocusClass(force) {
-    this.DOM.scope.classList.toggle('tagify--focus', !!force);
-  },
+    if (key) localStorage.removeItem(base + key); // delete all keys in the storage with a specific tagify id
+    else {
+      for (let k in localStorage) if (k.includes(base)) localStorage.removeItem(k);
+    }
+  };
 
-  /**
-   * DOM events listeners binding
-   */
-  events: {
-    // bind custom events which were passed in the settings
-    customBinding: function customBinding() {
-      var _this3 = this;
+  var TEXTS = {
+    empty: "empty",
+    exceed: "number of tags exceeded",
+    pattern: "pattern mismatch",
+    duplicate: "already exists",
+    notAllowed: "not allowed"
+  };
+
+  var templates = {
+    /**
+     *
+     * @param {DOM Object} input     Original input DOm element
+     * @param {Object}     settings  Tagify instance settings Object
+     */
+    wrapper(input, _s) {
+      return `<tags class="${_s.classNames.namespace} ${_s.mode ? `${_s.classNames[_s.mode + "Mode"]}` : ""} ${input.className}"
+                    ${_s.readonly ? 'readonly' : ''}
+                    ${_s.disabled ? 'disabled' : ''}
+                    ${_s.required ? 'required' : ''}
+                    tabIndex="-1">
+            <span ${!_s.readonly && _s.userInput ? 'contenteditable' : ''} tabIndex="0" data-placeholder="${_s.placeholder || '&#8203;'}" aria-placeholder="${_s.placeholder || ''}"
+                class="${_s.classNames.input}"
+                role="textbox"
+                aria-autocomplete="both"
+                aria-multiline="${_s.mode == 'mix' ? true : false}"></span>
+                &#8203;
+        </tags>`;
+    },
+
+    tag(tagData, tagify) {
+      var _s = this.settings;
+      return `<tag title="${tagData.title || tagData.value}"
+                    contenteditable='false'
+                    spellcheck='false'
+                    tabIndex="${_s.a11y.focusableTags ? 0 : -1}"
+                    class="${_s.classNames.tag} ${tagData.class || ""}"
+                    ${this.getAttributes(tagData)}>
+            <x title='' class="${_s.classNames.tagX}" role='button' aria-label='remove tag'></x>
+            <div>
+                <span class="${_s.classNames.tagText}">${tagData[_s.tagTextProp] || tagData.value}</span>
+            </div>
+        </tag>`;
+    },
+
+    dropdown(settings) {
+      var _sd = settings.dropdown,
+          isManual = _sd.position == 'manual',
+          className = `${settings.classNames.dropdown}`;
+      return `<div class="${isManual ? "" : className} ${_sd.classname}" role="listbox" aria-labelledby="dropdown">
+                    <div class="${settings.classNames.dropdownWrapper}"></div>
+                </div>`;
+    },
+
+    dropdownItem(item, tagify) {
+      return `<div ${this.getAttributes(item)}
+                    class='${this.settings.classNames.dropdownItem} ${item.class ? item.class : ""}'
+                    tabindex="0"
+                    role="option">${item.value}</div>`;
+    },
+
+    dropdownItemNoMatch: null
+  };
+
+  function EventDispatcher(instance) {
+    // Create a DOM EventTarget object
+    var target = document.createTextNode('');
+
+    function addRemove(op, events, cb) {
+      if (cb) events.split(/\s+/g).forEach(name => target[op + 'EventListener'].call(target, name, cb));
+    } // Pass EventTarget interface calls to DOM EventTarget object
+
+
+    return {
+      off(events, cb) {
+        addRemove('remove', events, cb);
+        return this;
+      },
+
+      on(events, cb) {
+        if (cb && typeof cb == 'function') addRemove('add', events, cb);
+        return this;
+      },
+
+      trigger(eventName, data, opts) {
+        var e;
+        opts = opts || {
+          cloneData: true
+        };
+        if (!eventName) return;
+
+        if (instance.settings.isJQueryPlugin) {
+          if (eventName == 'remove') eventName = 'removeTag'; // issue #222
+
+          jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data]);
+        } else {
+          try {
+            var eventData = typeof data === 'object' ? data : {
+              value: data
+            };
+            eventData = opts.cloneData ? extend({}, eventData) : eventData;
+            eventData.tagify = this; // TODO: move the below to the "extend" function
+
+            if (data instanceof Object) for (var prop in data) if (data[prop] instanceof HTMLElement) eventData[prop] = data[prop];
+            e = new CustomEvent(eventName, {
+              "detail": eventData
+            });
+          } catch (err) {
+            console.warn(err);
+          }
+
+          target.dispatchEvent(e);
+        }
+      }
+
+    };
+  }
+
+  var deleteBackspaceTimeout;
+  function triggerChangeEvent() {
+    if (this.settings.mixMode.integrated) return;
+    var inputElm = this.DOM.originalInput,
+        changed = this.state.lastOriginalValueReported !== inputElm.value,
+        event = new CustomEvent("change", {
+      bubbles: true
+    }); // must use "CustomEvent" and not "Event" to support IE
+
+    if (!changed) return; // must apply this BEFORE triggering the simulated event
+
+    this.state.lastOriginalValueReported = inputElm.value; // React hack: https://github.com/facebook/react/issues/11488
+
+    event.simulated = true;
+    if (inputElm._valueTracker) inputElm._valueTracker.setValue(Math.random());
+    inputElm.dispatchEvent(event); // also trigger a Tagify event
 
-      this.customEventsList.forEach(function (name) {
-        _this3.on(name, _this3.settings.callbacks[name]);
+    this.trigger("change", this.state.lastOriginalValueReported); // React, for some reason, clears the input's value after "dispatchEvent" is fired
+
+    inputElm.value = this.state.lastOriginalValueReported;
+  }
+  var events = {
+    // bind custom events which were passed in the settings
+    customBinding() {
+      this.customEventsList.forEach(name => {
+        this.on(name, this.settings.callbacks[name]);
       });
     },
-    binding: function binding() {
-      var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
 
+    binding(bindUnbind = true) {
       var _CB = this.events.callbacks,
           _CBR,
           action = bindUnbind ? 'addEventListener' : 'removeEventListener'; // do not allow the main events to be bound more than once
@@ -463,97 +1161,193 @@ Tagify.prototype = {
 
       if (this.state.mainEvents && bindUnbind) return; // set the binding state of the main events, so they will not be bound more than once
 
-      this.state.mainEvents = bindUnbind;
+      this.state.mainEvents = bindUnbind; // everything inside gets executed only once-per instance
 
       if (bindUnbind && !this.listeners.main) {
-        // this event should never be unbinded:
-        // IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead..
-        this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this));
+        this.events.bindGlobal.call(this);
         if (this.settings.isJQueryPlugin) jQuery(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
       } // setup callback references so events could be removed later
 
 
       _CBR = this.listeners.main = this.listeners.main || {
         focus: ['input', _CB.onFocusBlur.bind(this)],
-        blur: ['input', _CB.onFocusBlur.bind(this)],
         keydown: ['input', _CB.onKeydown.bind(this)],
         click: ['scope', _CB.onClickScope.bind(this)],
-        dblclick: ['scope', _CB.onDoubleClickScope.bind(this)]
+        dblclick: ['scope', _CB.onDoubleClickScope.bind(this)],
+        paste: ['input', _CB.onPaste.bind(this)],
+        drop: ['input', _CB.onDrop.bind(this)]
       };
 
       for (var eventName in _CBR) {
-        // make sure the focus/blur event is always regesitered (and never more than once)
-        if (eventName == 'blur' && !bindUnbind) return;
-
         this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
-      }
+      } // listen to original input changes (unfortunetly this is the best way...)
+      // https://stackoverflow.com/a/1949416/104380
+
+
+      clearInterval(this.listeners.main.originalInputValueObserverInterval);
+      this.listeners.main.originalInputValueObserverInterval = setInterval(_CB.observeOriginalInputValue.bind(this), 500); // observers
+
+      var inputMutationObserver = this.listeners.main.inputMutationObserver || new MutationObserver(_CB.onInputDOMChange.bind(this)); // cleaup just-in-case
+
+      if (inputMutationObserver) inputMutationObserver.disconnect(); // observe stuff
+
+      if (this.settings.mode == 'mix') inputMutationObserver.observe(this.DOM.input, {
+        childList: true
+      });
+    },
+
+    bindGlobal(unbind) {
+      var _CB = this.events.callbacks,
+          action = unbind ? 'removeEventListener' : 'addEventListener',
+          e;
+      if (!unbind && this.listeners.global) return; // do not re-bind
+      // these events are global event should never be unbinded, unless the instance is destroyed:
+
+      this.listeners.global = this.listeners && this.listeners.global || [{
+        type: this.isIE ? 'keydown' : 'input',
+        // IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead..
+        target: this.DOM.input,
+        cb: _CB[this.isIE ? 'onInputIE' : 'onInput'].bind(this)
+      }, {
+        type: 'keydown',
+        target: window,
+        cb: _CB.onWindowKeyDown.bind(this)
+      }, {
+        type: 'blur',
+        target: this.DOM.input,
+        cb: _CB.onFocusBlur.bind(this)
+      }];
+
+      for (e of this.listeners.global) e.target[action](e.type, e.cb);
+    },
+
+    unbindGlobal() {
+      this.events.bindGlobal.call(this, true);
     },
 
     /**
      * DOM events callbacks
      */
     callbacks: {
-      onFocusBlur: function onFocusBlur(e) {
-        var text = e.target ? e.target.textContent.trim() : '',
+      onFocusBlur(e) {
+        var text = e.target ? this.trim(e.target.textContent) : '',
             // a string
         _s = this.settings,
-            type = e.type; // goes into this scenario only on input "blur" and a tag was clicked
-
-        if (e.relatedTarget && e.relatedTarget.classList.contains('tagify__tag') && this.DOM.scope.contains(e.relatedTarget)) return;
+            type = e.type,
+            ddEnabled = _s.dropdown.enabled >= 0,
+            eventData = {
+          relatedTarget: e.relatedTarget
+        },
+            isTargetSelectOption = this.state.actions.selectOption && (ddEnabled || !_s.dropdown.closeOnSelect),
+            isTargetAddNewBtn = this.state.actions.addNew && ddEnabled,
+            isRelatedTargetX = e.relatedTarget && isNodeTag.call(this, e.relatedTarget) && this.DOM.scope.contains(e.relatedTarget),
+            shouldAddTags;
+
+        if (type == 'blur') {
+          if (e.relatedTarget === this.DOM.scope) {
+            this.dropdown.hide();
+            this.DOM.input.focus();
+            return;
+          }
 
-        if (type == 'blur' && e.relatedTarget === this.DOM.scope) {
-          this.dropdown.hide.call(this);
-          this.DOM.input.focus();
-          return;
+          this.postUpdate();
+          this.triggerChangeEvent();
         }
 
-        if (this.state.actions.selectOption && (_s.dropdown.enabled || !_s.dropdown.closeOnSelect)) return;
+        if (isTargetSelectOption || isTargetAddNewBtn) return;
         this.state.hasFocus = type == "focus" ? +new Date() : false;
         this.toggleFocusClass(this.state.hasFocus);
-        this.setRangeAtStartEnd(false);
 
         if (_s.mode == 'mix') {
-          if (e.type == "blur") this.dropdown.hide.call(this);
+          if (type == "focus") {
+            this.trigger("focus", eventData);
+          } else if (e.type == "blur") {
+            this.trigger("blur", eventData);
+            this.loading(false);
+            this.dropdown.hide(); // reset state which needs reseting
+
+            this.state.dropdown.visible = undefined;
+            this.setStateSelection();
+          }
+
           return;
         }
 
-        if (type == "focus") {
-          this.trigger("focus", {
-            relatedTarget: e.relatedTarget
-          }); //  e.target.classList.remove('placeholder');
+        if (type == "focus") {
+          this.trigger("focus", eventData); //  e.target.classList.remove('placeholder');
+
+          if (_s.dropdown.enabled === 0 || !_s.userInput) {
+            // && _s.mode != "select"
+            this.dropdown.show(this.value.length ? '' : undefined);
+          }
+
+          return;
+        } else if (type == "blur") {
+          this.trigger("blur", eventData);
+          this.loading(false); // when clicking the X button of a selected tag, it is unwanted it will be added back
+          // again in a few more lines of code (shouldAddTags && addTags)
+
+          if (this.settings.mode == 'select' && isRelatedTargetX) text = '';
+          shouldAddTags = this.settings.mode == 'select' && text ? !this.value.length || this.value[0].value != text : text && !this.state.actions.selectOption && _s.addTagOnBlur; // do not add a tag if "selectOption" action was just fired (this means a tag was just added from the dropdown)
+
+          shouldAddTags && this.addTags(text, true);
+          if (this.settings.mode == 'select' && !text) this.removeTags();
+        }
+
+        this.DOM.input.removeAttribute('style');
+        this.dropdown.hide();
+      },
+
+      onWindowKeyDown(e) {
+        var focusedElm = document.activeElement,
+            isTag = isNodeTag.call(this, focusedElm),
+            isBelong = isTag && this.DOM.scope.contains(document.activeElement),
+            nextTag;
+        if (!isBelong) return;
+        nextTag = focusedElm.nextElementSibling;
 
-          if (_s.dropdown.enabled === 0 && _s.mode != "select") {
-            this.dropdown.show.call(this);
-          }
+        switch (e.key) {
+          // remove tag if has focus
+          case 'Backspace':
+            {
+              if (!this.settings.readonly) {
+                this.removeTags(focusedElm);
+                (nextTag ? nextTag : this.DOM.input).focus();
+              }
 
-          return;
-        } else if (type == "blur") {
-          this.trigger("blur", {
-            relatedTarget: e.relatedTarget
-          });
-          this.loading(false); // do not add a tag if "selectOption" action was just fired (this means a tag was just added from the dropdown)
+              break;
+            }
+          // edit tag if has focus
 
-          text && !this.state.actions.selectOption && _s.addTagOnBlur && this.addTags(text, true);
+          case 'Enter':
+            {
+              setTimeout(this.editTag.bind(this), 0, focusedElm);
+              break;
+            }
         }
-
-        this.DOM.input.removeAttribute('style');
-        this.dropdown.hide.call(this);
       },
-      onKeydown: function onKeydown(e) {
-        var _this4 = this;
 
-        var s = e.target.textContent.trim(),
-            tags;
+      onKeydown(e) {
+        var _s = this.settings;
+
+        if (_s.mode == 'select' && _s.enforceWhitelist && this.value.length && e.key != 'Tab') {
+          e.preventDefault();
+        }
+
+        var s = this.trim(e.target.textContent);
         this.trigger("keydown", {
           originalEvent: this.cloneEvent(e)
         });
+        /**
+         * ONLY FOR MIX-MODE:
+         */
 
-        if (this.settings.mode == 'mix') {
+        if (_s.mode == 'mix') {
           switch (e.key) {
             case 'Left':
             case 'ArrowLeft':
               {
-                // when left arrow was pressed, raise a flag so when the dropdown is shown, right-arrow will be ignored
+                // when left arrow was pressed, set a flag so when the dropdown is shown, right-arrow will be ignored
                 // because it seems likely the user wishes to use the arrows to move the caret
                 this.state.actions.ArrowLeft = true;
                 break;
@@ -562,28 +1356,125 @@ Tagify.prototype = {
             case 'Delete':
             case 'Backspace':
               {
-                var selection = document.getSelection(),
-                    isFF = !!navigator.userAgent.match(/firefox/i);
-                if (isFF && selection && selection.anchorOffset == 0) this.removeTag(selection.anchorNode.previousSibling);
-                var values = []; // find out which tag(s) were deleted and update "this.value" accordingly
+                if (this.state.editing) return;
+                var sel = document.getSelection(),
+                    deleteKeyTagDetected = e.key == 'Delete' && sel.anchorOffset == (sel.anchorNode.length || 0),
+                    prevAnchorSibling = sel.anchorNode.previousSibling,
+                    isCaretAfterTag = sel.anchorNode.nodeType == 1 || !sel.anchorOffset && prevAnchorSibling && prevAnchorSibling.nodeType == 1 && sel.anchorNode.previousSibling,
+                    lastInputValue = decode(this.DOM.input.innerHTML),
+                    lastTagElems = this.getTagElms(),
+                    //  isCaretInsideTag = sel.anchorNode.parentNode('.' + _s.classNames.tag),
+                tagBeforeCaret,
+                    tagElmToBeDeleted,
+                    firstTextNodeBeforeTag;
+
+                if (_s.backspace == 'edit' && isCaretAfterTag) {
+                  tagBeforeCaret = sel.anchorNode.nodeType == 1 ? null : sel.anchorNode.previousElementSibling;
+                  setTimeout(this.editTag.bind(this), 0, tagBeforeCaret); // timeout is needed to the last cahacrter in the edited tag won't get deleted
+
+                  e.preventDefault(); // needed so the tag elm won't get deleted
+
+                  return;
+                }
+
+                if (isChromeAndroidBrowser() && isCaretAfterTag) {
+                  firstTextNodeBeforeTag = getfirstTextNode(isCaretAfterTag);
+                  if (!isCaretAfterTag.hasAttribute('readonly')) isCaretAfterTag.remove(); // since this is Chrome, can safetly use this "new" DOM API
+                  // Android-Chrome wrongly hides the keyboard, and loses focus,
+                  // so this hack below is needed to regain focus at the correct place:
+
+                  this.DOM.input.focus();
+                  setTimeout(() => {
+                    this.placeCaretAfterNode(firstTextNodeBeforeTag);
+                    this.DOM.input.click();
+                  });
+                  return;
+                }
+
+                if (sel.anchorNode.nodeName == 'BR') return;
+                if ((deleteKeyTagDetected || isCaretAfterTag) && sel.anchorNode.nodeType == 1) {
+                  if (sel.anchorOffset == 0) // caret is at the very begining, before a tag
+                    tagElmToBeDeleted = deleteKeyTagDetected // delete key pressed
+                    ? lastTagElems[0] : null;else tagElmToBeDeleted = lastTagElems[sel.anchorOffset - 1]; // find out if a tag *might* be a candidate for deletion, and if so, which
+
+                } else if (deleteKeyTagDetected) tagElmToBeDeleted = sel.anchorNode.nextElementSibling;else if (isCaretAfterTag) tagElmToBeDeleted = isCaretAfterTag; // tagElm.hasAttribute('readonly')
+
+                if (sel.anchorNode.nodeType == 3 && // node at caret location is a Text node
+                !sel.anchorNode.nodeValue && // has some text
+                sel.anchorNode.previousElementSibling) // text node has a Tag node before it
+                  e.preventDefault(); // if backspace not allowed, do nothing
+                // TODO: a better way to detect if nodes were deleted is to simply check the "this.value" before & after
+
+                if ((isCaretAfterTag || deleteKeyTagDetected) && !_s.backspace) {
+                  e.preventDefault();
+                  return;
+                }
+
+                if (sel.type != 'Range' && !sel.anchorOffset && sel.anchorNode == this.DOM.input && e.key != 'Delete') {
+                  e.preventDefault();
+                  return;
+                }
+
+                if (sel.type != 'Range' && tagElmToBeDeleted && tagElmToBeDeleted.hasAttribute('readonly')) {
+                  // allows the continuation of deletion by placing the caret on the first previous textNode.
+                  // since a few readonly-tags might be one after the other, iteration is needed:
+                  this.placeCaretAfterNode(getfirstTextNode(tagElmToBeDeleted));
+                  return;
+                } // update regarding https://github.com/yairEO/tagify/issues/762#issuecomment-786464317:
+                // the bug described is more severe than the fix below, therefore I disable the fix until a solution
+                // is found which work well for both cases.
+                // -------
+                // nodeType is "1" only when the caret is at the end after last tag (no text after), or before first first (no text before)
+
+                /*
+                if( this.isFirefox && sel.anchorNode.nodeType == 1 && sel.anchorOffset != 0 ){
+                    this.removeTags() // removes last tag by default if no parameter supplied
+                    // place caret inside last textNode, if exist. it's an annoying bug only in FF,
+                    // if the last tag is removed, and there is a textNode before it, the caret is not placed at its end
+                    this.placeCaretAfterNode( this.setRangeAtStartEnd() )
+                }
+                */
+
 
-                tags = this.DOM.input.children; // a minimum delay is needed before the node actually gets ditached from the document (don't know why),
+                clearTimeout(deleteBackspaceTimeout); // a minimum delay is needed before the node actually gets detached from the document (don't know why),
                 // to know exactly which tag was deleted. This is the easiest way of knowing besides using MutationObserver
 
-                setTimeout(function () {
+                deleteBackspaceTimeout = setTimeout(() => {
+                  var sel = document.getSelection(),
+                      currentValue = decode(this.DOM.input.innerHTML),
+                      prevElm = !deleteKeyTagDetected && sel.anchorNode.previousSibling; // fixes #384, where the first and only tag will not get removed with backspace
+
+                  if (currentValue.length >= lastInputValue.length && prevElm) {
+                    if (isNodeTag.call(this, prevElm) && !prevElm.hasAttribute('readonly')) {
+                      this.removeTags(prevElm);
+                      this.fixFirefoxLastTagNoCaret(); // the above "removeTag" methods removes the tag with a transition. Chrome adds a <br> element for some reason at this stage
+
+                      if (this.DOM.input.children.length == 2 && this.DOM.input.children[1].tagName == "BR") {
+                        this.DOM.input.innerHTML = "";
+                        this.value.length = 0;
+                        return true;
+                      }
+                    } else prevElm.remove();
+                  } // find out which tag(s) were deleted and trigger "remove" event
                   // iterate over the list of tags still in the document and then filter only those from the "this.value" collection
-                  [].forEach.call(tags, function (tagElm) {
-                    return values.push(tagElm.getAttribute('value'));
-                  });
-                  _this4.value = _this4.value.filter(function (d) {
-                    return values.indexOf(d.value) != -1;
-                  });
-                });
+
+
+                  this.value = [].map.call(lastTagElems, (node, nodeIdx) => {
+                    var tagData = this.tagData(node); // since readonly cannot be removed (it's technically resurrected if removed somehow)
+
+                    if (node.parentNode || tagData.readonly) return tagData;else this.trigger('remove', {
+                      tag: node,
+                      index: nodeIdx,
+                      data: tagData
+                    });
+                  }).filter(n => n); // remove empty items in the mapped array
+                }, 20); // Firefox needs this higher duration for some reason or things get buggy when deleting text from the end
+
                 break;
               }
             // currently commented to allow new lines in mixed-mode
             // case 'Enter' :
-            //     e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
+            //     // e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
           }
 
           return true;
@@ -591,11 +1482,12 @@ Tagify.prototype = {
 
         switch (e.key) {
           case 'Backspace':
-            if (s == "" || s.charCodeAt(0) == 8203) {
-              // 8203: ZERO WIDTH SPACE unicode
-              if (this.settings.backspace === true) this.removeTag();else if (this.settings.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); // timeout reason: when edited tag gets focused and the caret is placed at the end, the last character gets deletec (because of backspace)
+            if (_s.mode == 'select' && _s.enforceWhitelist && this.value.length) this.removeTags();else if (!this.state.dropdown.visible || _s.dropdown.position == 'manual') {
+              if (e.target.textContent == "" || s.charCodeAt(0) == 8203) {
+                // 8203: ZERO WIDTH SPACE unicode
+                if (_s.backspace === true) this.removeTags();else if (_s.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); // timeout reason: when edited tag gets focused and the caret is placed at the end, the last character gets deletec (because of backspace)
+              }
             }
-
             break;
 
           case 'Esc':
@@ -606,15 +1498,15 @@ Tagify.prototype = {
 
           case 'Down':
           case 'ArrowDown':
-            // if( this.settings.mode == 'select' ) // issue #333
-            if (!this.state.dropdown.visible) this.dropdown.show.call(this);
+            // if( _s.mode == 'select' ) // issue #333
+            if (!this.state.dropdown.visible) this.dropdown.show();
             break;
 
           case 'ArrowRight':
             {
-              var tagData = this.state.inputSuggestion || this.state.ddItemData;
+              let tagData = this.state.inputSuggestion || this.state.ddItemData;
 
-              if (tagData && this.settings.autoComplete.rightKey) {
+              if (tagData && _s.autoComplete.rightKey) {
                 this.addTags([tagData], true);
                 return;
               }
@@ -624,41 +1516,40 @@ Tagify.prototype = {
 
           case 'Tab':
             {
-              if (!s) return true;
+              let selectMode = _s.mode == 'select';
+              if (s && !selectMode) e.preventDefault();else return true;
             }
 
           case 'Enter':
+            if (this.state.dropdown.visible || e.keyCode == 229) return;
             e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
             // because the main "keydown" event is bound before the dropdown events, this will fire first and will not *yet*
             // know if an option was just selected from the dropdown menu. If an option was selected,
             // the dropdown events should handle adding the tag
 
-            setTimeout(function () {
-              if (_this4.state.actions.selectOption) return;
-
-              _this4.addTags(s, true);
+            setTimeout(() => {
+              if (this.state.actions.selectOption) return;
+              this.addTags(s, true);
             });
         }
       },
-      onInput: function onInput(e) {
-        var value = this.settings.mode == 'mix' ? this.DOM.input.textContent : this.input.normalize.call(this),
+
+      onInput(e) {
+        this.postUpdate(); // toggles "tagify--empty" class
+
+        if (this.settings.mode == 'mix') return this.events.callbacks.onMixTagsInput.call(this, e);
+        var value = this.input.normalize.call(this),
             showSuggestions = value.length >= this.settings.dropdown.enabled,
-            data = {
-          value: value,
+            eventData = {
+          value,
           inputElm: this.DOM.input
         };
-        if (this.settings.mode == 'mix') return this.events.callbacks.onMixTagsInput.call(this, e);
-
-        if (!value) {
-          this.input.set.call(this, '');
-          return;
-        }
+        eventData.isValid = this.validateTag({
+          value
+        }); // for IE; since IE doesn't have an "input" event so "keyDown" is used instead to trigger the "onInput" callback,
+        // and so many keys do not change the input, and for those do not continue.
 
-        if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead
-
-        data.isValid = this.validateTag(value);
-        this.trigger('input', data); // "input" event must be triggered at this point, before the dropdown is shown
-        // save the value on the input's State object
+        if (this.state.inputText == value) return; // save the value on the input's State object
 
         this.input.set.call(this, value, false); // update the input with the normalized value and run validations
         // this.setRangeAtStartEnd(); // fix caret position
@@ -668,56 +1559,133 @@ Tagify.prototype = {
             this.input.set.call(this); // clear the input field's value
           }
         } else if (this.settings.dropdown.enabled >= 0) {
-          this.dropdown[showSuggestions ? "show" : "hide"].call(this, value);
+          this.dropdown[showSuggestions ? "show" : "hide"](value);
         }
+
+        this.trigger('input', eventData); // "input" event must be triggered at this point, before the dropdown is shown
       },
-      onMixTagsInput: function onMixTagsInput(e) {
-        var _this5 = this;
 
-        var sel,
-            range,
-            split,
+      onMixTagsInput(e) {
+        var rangeText,
+            match,
+            matchedPatternCount,
             tag,
             showSuggestions,
-            _s = this.settings;
+            selection,
+            _s = this.settings,
+            lastTagsCount = this.value.length,
+            matchFlaggedTag,
+            matchDelimiters,
+            tagsElems = this.getTagElms(),
+            fragment = document.createDocumentFragment(),
+            range = window.getSelection().getRangeAt(0),
+            remainingTagsValues = [].map.call(tagsElems, node => this.tagData(node).value); // Android Chrome "keydown" event argument does not report the correct "key".
+        // this workaround is needed to manually call "onKeydown" method with a synthesized event object
+
+        if (e.inputType == "deleteContentBackward" && isChromeAndroidBrowser()) {
+          this.events.callbacks.onKeydown.call(this, {
+            target: e.target,
+            key: "Backspace"
+          });
+        } // re-add "readonly" tags which might have been removed
+
+
+        this.value.slice().forEach(item => {
+          if (item.readonly && !remainingTagsValues.includes(item.value)) fragment.appendChild(this.createTagElem(item));
+        });
+
+        if (fragment.childNodes.length) {
+          range.insertNode(fragment);
+          this.setRangeAtStartEnd(false, fragment.lastChild);
+        } // check if tags were "magically" added/removed (browser redo/undo or CTRL-A -> delete)
+
+
+        if (tagsElems.length != lastTagsCount) {
+          this.value = [].map.call(this.getTagElms(), node => this.tagData(node));
+          this.update({
+            withoutChangeEvent: true
+          });
+          return;
+        }
+
         if (this.hasMaxTags()) return true;
 
         if (window.getSelection) {
-          sel = window.getSelection();
+          selection = window.getSelection(); // only detect tags if selection is inside a textNode (not somehow on already-existing tag)
 
-          if (sel.rangeCount > 0) {
-            range = sel.getRangeAt(0).cloneRange();
+          if (selection.rangeCount > 0 && selection.anchorNode.nodeType == 3) {
+            range = selection.getRangeAt(0).cloneRange();
             range.collapse(true);
-            range.setStart(window.getSelection().focusNode, 0);
-            split = range.toString().split(_s.mixTagsAllowedAfter); // ["foo", "bar", "@a"]
+            range.setStart(selection.focusNode, 0);
+            rangeText = range.toString().slice(0, range.endOffset); // slice the range so everything AFTER the caret will be trimmed
+            // split = range.toString().split(_s.mixTagsAllowedAfter)  // ["foo", "bar", "@baz"]
 
-            tag = split[split.length - 1].match(_s.pattern);
+            matchedPatternCount = rangeText.split(_s.pattern).length - 1;
+            match = rangeText.match(_s.pattern);
+            if (match) // tag string, example: "@aaa ccc"
+              tag = rangeText.slice(rangeText.lastIndexOf(match[match.length - 1]));
 
             if (tag) {
               this.state.actions.ArrowLeft = false; // start fresh, assuming the user did not (yet) used any arrow to move the caret
 
               this.state.tag = {
-                prefix: tag[0],
-                value: tag.input.split(tag[0])[1]
+                prefix: tag.match(_s.pattern)[0],
+                value: tag.replace(_s.pattern, '') // get rid of the prefix
+
               };
-              showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled;
+              this.state.tag.baseOffset = selection.baseOffset - this.state.tag.value.length;
+              matchDelimiters = this.state.tag.value.match(_s.delimiters); // if a delimeter exists, add the value as tag (exluding the delimiter)
+
+              if (matchDelimiters) {
+                this.state.tag.value = this.state.tag.value.replace(_s.delimiters, '');
+                this.state.tag.delimiters = matchDelimiters[0];
+                this.addTags(this.state.tag.value, _s.dropdown.clearOnSelect);
+                this.dropdown.hide();
+                return;
+              }
+
+              showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled; // When writeing something that might look like a tag (an email address) but isn't one - it is unwanted
+              // the suggestions dropdown be shown, so the user closes it (in any way), and while continue typing,
+              // dropdown should stay closed until another tag is typed.
+              // if( this.state.tag.value.length && this.state.dropdown.visible === false )
+              //     showSuggestions = false
+              // test for similar flagged tags to the current tag
+
+              try {
+                matchFlaggedTag = this.state.flaggedTags[this.state.tag.baseOffset];
+                matchFlaggedTag = matchFlaggedTag.prefix == this.state.tag.prefix && matchFlaggedTag.value[0] == this.state.tag.value[0]; // reset
+
+                if (this.state.flaggedTags[this.state.tag.baseOffset] && !this.state.tag.value) delete this.state.flaggedTags[this.state.tag.baseOffset];
+              } catch (err) {} // scenario: (do not show suggestions of previous matched tag, if more than 1 detected)
+              // (2 tags exist)                          " a@a.com and @"
+              // (second tag is removed by backspace)    " a@a.com and "
+
+
+              if (matchFlaggedTag || matchedPatternCount < this.state.mixMode.matchedPatternCount) showSuggestions = false;
+            } // no (potential) tag found
+            else {
+              this.state.flaggedTags = {};
             }
-          }
-        }
 
-        this.update(); // wait until the "this.value" has been updated (see "onKeydown" method for "mix-mode")
+            this.state.mixMode.matchedPatternCount = matchedPatternCount;
+          }
+        } // wait until the "this.value" has been updated (see "onKeydown" method for "mix-mode")
         // the dropdown must be shown only after this event has been driggered, so an implementer could
         // dynamically change the whitelist.
 
-        setTimeout(function () {
-          _this5.trigger("input", _this5.extend({}, _this5.state.tag, {
-            textContent: _this5.DOM.input.textContent
-          }));
 
-          if (_this5.state.tag) _this5.dropdown[showSuggestions ? "show" : "hide"].call(_this5, _this5.state.tag.value);
+        setTimeout(() => {
+          this.update({
+            withoutChangeEvent: true
+          });
+          this.trigger("input", extend({}, this.state.tag, {
+            textContent: this.DOM.input.textContent
+          }));
+          if (this.state.tag) this.dropdown[showSuggestions ? "show" : "hide"](this.state.tag.value);
         }, 10);
       },
-      onInputIE: function onInputIE(e) {
+
+      onInputIE(e) {
         var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay
 
 
@@ -725,93 +1693,203 @@ Tagify.prototype = {
           _this.events.callbacks.onInput.call(_this, e);
         });
       },
-      onClickScope: function onClickScope(e) {
-        var tagElm = e.target.closest('.tagify__tag'),
-            _s = this.settings,
-            timeDiffFocus = +new Date() - this.state.hasFocus,
-            tagElmIdx;
+
+      observeOriginalInputValue() {
+        // if original input value changed for some reason (for exmaple a form reset)
+        if (this.DOM.originalInput.value != this.DOM.originalInput.tagifyValue) this.loadOriginalValues();
+      },
+
+      onClickScope(e) {
+        var _s = this.settings,
+            tagElm = e.target.closest('.' + _s.classNames.tag),
+            timeDiffFocus = +new Date() - this.state.hasFocus;
 
         if (e.target == this.DOM.scope) {
-          // if( !this.state.hasFocus )
-          //   this.dropdown.hide.call(this)
-          this.DOM.input.focus();
+          if (!this.state.hasFocus) this.DOM.input.focus();
           return;
-        } else if (e.target.classList.contains("tagify__tag__removeBtn")) {
-          this.removeTag(e.target.parentNode);
+        } else if (e.target.classList.contains(_s.classNames.tagX)) {
+          this.removeTags(e.target.parentNode);
           return;
         } else if (tagElm) {
-          tagElmIdx = this.getNodeIndex(tagElm);
           this.trigger("click", {
             tag: tagElm,
-            index: tagElmIdx,
-            data: this.value[tagElmIdx],
+            index: this.getNodeIndex(tagElm),
+            data: this.tagData(tagElm),
             originalEvent: this.cloneEvent(e)
           });
-          if (this.settings.editTags == 1) this.events.callbacks.onDoubleClickScope.call(this, e);
+          if (_s.editTags === 1 || _s.editTags.clicks === 1) this.events.callbacks.onDoubleClickScope.call(this, e);
           return;
         } // when clicking on the input itself
-        else if (e.target == this.DOM.input && timeDiffFocus > 500) {
-            if (this.state.dropdown.visible) this.dropdown.hide.call(this);else if (_s.dropdown.enabled === 0 && _s.mode != 'mix') this.dropdown.show.call(this);
+        else if (e.target == this.DOM.input) {
+          if (_s.mode == 'mix') {
+            // firefox won't show caret if last element is a tag (and not a textNode),
+            // so an empty textnode should be added
+            this.fixFirefoxLastTagNoCaret();
+          }
+
+          if (timeDiffFocus > 500) {
+            if (this.state.dropdown.visible) this.dropdown.hide();else if (_s.dropdown.enabled === 0 && _s.mode != 'mix') this.dropdown.show(this.value.length ? '' : undefined);
             return;
           }
+        }
+
+        if (_s.mode == 'select') !this.state.dropdown.visible && this.dropdown.show();
+      },
+
+      // special proccess is needed for pasted content in order to "clean" it
+      onPaste(e) {
+        e.preventDefault();
+        var _s = this.settings,
+            selectModeWithoutInput = _s.mode == 'select' && _s.enforceWhitelist;
+
+        if (selectModeWithoutInput || !_s.userInput) {
+          return false;
+        }
+
+        var clipboardData, pastedText;
+        if (_s.readonly) return; // Get pasted data via clipboard API
+
+        clipboardData = e.clipboardData || window.clipboardData;
+        pastedText = clipboardData.getData('Text');
+
+        _s.hooks.beforePaste(e, {
+          tagify: this,
+          pastedText,
+          clipboardData
+        }).then(result => {
+          if (result === undefined) result = pastedText;
+
+          if (result) {
+            this.injectAtCaret(result, window.getSelection().getRangeAt(0));
+
+            if (this.settings.mode == 'mix') {
+              this.events.callbacks.onMixTagsInput.call(this, e);
+            } else if (this.settings.pasteAsTags) {
+              this.addTags(this.state.inputText + result, true);
+            } else this.state.inputText = result;
+          }
+        }).catch(err => err);
+      },
 
-        if (_s.mode == 'select') !this.state.dropdown.visible && this.dropdown.show.call(this);
+      onDrop(e) {
+        e.preventDefault();
       },
-      onEditTagInput: function onEditTagInput(editableElm, e) {
-        var tagElm = editableElm.closest('tag'),
+
+      onEditTagInput(editableElm, e) {
+        var tagElm = editableElm.closest('.' + this.settings.classNames.tag),
             tagElmIdx = this.getNodeIndex(tagElm),
+            tagData = this.tagData(tagElm),
             value = this.input.normalize.call(this, editableElm),
-            isValid = value.toLowerCase() == editableElm.originalValue.toLowerCase() || this.validateTag(value);
-        tagElm.classList.toggle('tagify--invalid', isValid !== true);
-        tagElm.isValid = isValid; // show dropdown if typed text is equal or more than the "enabled" dropdown setting
+            hasChanged = tagElm.innerHTML != tagElm.__tagifyTagData.__originalHTML,
+            isValid = this.validateTag({
+          [this.settings.tagTextProp]: value
+        }); // the value could have been invalid in the first-place so make sure to re-validate it (via "addEmptyTag" method)
+        // 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
+
+        if (!hasChanged && editableElm.originalIsValid === true) isValid = true;
+        tagElm.classList.toggle(this.settings.classNames.tagInvalid, isValid !== true);
+        tagData.__isValid = isValid;
+        tagElm.title = isValid === true ? tagData.title || tagData.value : isValid; // change the tag's title to indicate why is the tag invalid (if it's so)
+        // show dropdown if typed text is equal or more than the "enabled" dropdown setting
 
         if (value.length >= this.settings.dropdown.enabled) {
-          this.state.editing.value = value;
-          this.dropdown.show.call(this, value);
+          // this check is needed apparently because doing browser "undo" will fire
+          //  "onEditTagInput" but "this.state.editing" will be "false"
+          if (this.state.editing) this.state.editing.value = value;
+          this.dropdown.show(value);
         }
 
         this.trigger("edit:input", {
           tag: tagElm,
           index: tagElmIdx,
-          data: this.extend({}, this.value[tagElmIdx], {
+          data: extend({}, this.value[tagElmIdx], {
             newValue: value
           }),
           originalEvent: this.cloneEvent(e)
         });
       },
-      onEditTagBlur: function onEditTagBlur(editableElm) {
-        if (!this.state.hasFocus) this.toggleFocusClass();
+
+      onEditTagFocus(tagElm) {
+        this.state.editing = {
+          scope: tagElm,
+          input: tagElm.querySelector("[contenteditable]")
+        };
+      },
+
+      onEditTagBlur(editableElm) {
+        if (!this.state.hasFocus) this.toggleFocusClass(); // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it
+        // the "onEditTagDone" is called directly, already replacing the tag, so the argument "editableElm"
+        // node isn't in the DOM anynmore because it has been replaced.
+
         if (!this.DOM.scope.contains(editableElm)) return;
 
-        var tagElm = editableElm.closest('.tagify__tag'),
-            tagElmIdx = this.getNodeIndex(tagElm),
-            currentValue = this.input.normalize.call(this, editableElm),
-            value = currentValue || editableElm.originalValue,
-            hasChanged = value != editableElm.originalValue,
-            isValid = tagElm.isValid,
-            tagData = _objectSpread({}, this.value[tagElmIdx], {
-          value: value
-        }); //  this.DOM.input.focus()
+        var _s = this.settings,
+            tagElm = editableElm.closest('.' + _s.classNames.tag),
+            textValue = this.input.normalize.call(this, editableElm),
+            originalData = this.tagData(tagElm).__originalData,
+            // pre-edit data
+        hasChanged = tagElm.innerHTML != tagElm.__tagifyTagData.__originalHTML,
+            isValid = this.validateTag({
+          [_s.tagTextProp]: textValue
+        }),
+            hasMaxTags,
+            newTagData; //  this.DOM.input.focus()
 
 
-        if (!currentValue) {
-          this.removeTag(tagElm);
+        if (!textValue) {
+          this.onEditTagDone(tagElm);
           return;
-        }
+        } // if nothing changed revert back to how it was before editing
 
-        if (hasChanged) {
-          this.settings.transformTag.call(this, tagData); // re-validate after tag transformation
 
-          isValid = this.validateTag(tagData.value);
-        } else {
-          this.onEditTagDone(tagElm);
+        if (!hasChanged) {
+          this.onEditTagDone(tagElm, originalData);
           return;
-        }
+        } // need to know this because if "keepInvalidTags" setting is "true" and an invalid tag is edited as a valid one,
+        // but the maximum number of tags have alreay been reached, so it should not allow saving the new valid value.
+        // only if the tag was already valid before editing, ignore this check (see a few lines below)
+
+
+        hasMaxTags = this.hasMaxTags();
+        newTagData = this.getWhitelistItem(textValue) || extend({}, originalData, {
+          [_s.tagTextProp]: textValue,
+          value: textValue,
+          __isValid: isValid
+        });
+
+        _s.transformTag.call(this, newTagData, originalData); // MUST re-validate after tag transformation
+        // only validate the "tagTextProp" because is the only thing that metters for validating an edited tag.
+        // -- Scenarios: --
+        // 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
+        // 2. max 3 tags allowed. there are 3 tags, one is edited, and so max-tags vaildation should be OK
+
+
+        isValid = (!hasMaxTags || originalData.__isValid === true) && this.validateTag({
+          [_s.tagTextProp]: newTagData[_s.tagTextProp]
+        });
 
-        if (isValid !== undefined && isValid !== true) return;
-        this.onEditTagDone(tagElm, tagData);
+        if (isValid !== true) {
+          this.trigger("invalid", {
+            data: newTagData,
+            tag: tagElm,
+            message: isValid
+          }); // do nothing if invalid, stay in edit-mode until corrected or reverted by presssing esc
+
+          if (_s.editTags.keepInvalid) return;
+          if (_s.keepInvalidTags) newTagData.__isValid = isValid;else // revert back if not specified to keep
+            newTagData = originalData;
+        } else if (_s.keepInvalidTags) {
+          // cleaup any previous leftovers if the tag was
+          delete newTagData.title;
+          delete newTagData["aria-invalid"];
+          delete newTagData.class;
+        } // tagElm.classList.toggle(_s.classNames.tagInvalid, true)
+
+
+        this.onEditTagDone(tagElm, newTagData);
       },
-      onEditTagkeydown: function onEditTagkeydown(e) {
+
+      onEditTagkeydown(e, tagElm) {
         this.trigger("edit:keydown", {
           originalEvent: this.cloneEvent(e)
         });
@@ -819,7 +1897,7 @@ Tagify.prototype = {
         switch (e.key) {
           case 'Esc':
           case 'Escape':
-            e.target.textContent = e.target.originalValue;
+            tagElm.innerHTML = tagElm.__tagifyTagData.__originalHTML;
 
           case 'Enter':
           case 'Tab':
@@ -827,1135 +1905,1569 @@ Tagify.prototype = {
             e.target.blur();
         }
       },
-      onDoubleClickScope: function onDoubleClickScope(e) {
-        var tagElm = e.target.closest('tag'),
+
+      onDoubleClickScope(e) {
+        var tagElm = e.target.closest('.' + this.settings.classNames.tag),
             _s = this.settings,
             isEditingTag,
             isReadyOnlyTag;
-        if (!tagElm) return;
-        isEditingTag = tagElm.classList.contains('tagify__tag--editable'), isReadyOnlyTag = tagElm.hasAttribute('readonly');
+        if (!tagElm || !_s.userInput) return;
+        isEditingTag = tagElm.classList.contains(this.settings.classNames.tagEditing);
+        isReadyOnlyTag = tagElm.hasAttribute('readonly');
         if (_s.mode != 'select' && !_s.readonly && !isEditingTag && !isReadyOnlyTag && this.settings.editTags) this.editTag(tagElm);
         this.toggleFocusClass(true);
+        this.trigger('dblclick', {
+          tag: tagElm,
+          index: this.getNodeIndex(tagElm),
+          data: this.tagData(tagElm)
+        });
+      },
+
+      /**
+       *
+       * @param {Object} m an object representing the observed DOM changes
+       */
+      onInputDOMChange(m) {
+        // iterate all DOm mutation
+        m.forEach(record => {
+          // only the ADDED nodes
+          record.addedNodes.forEach(addedNode => {
+            if (addedNode) {
+              // fix chrome's placing '<div><br></div>' everytime ENTER key is pressed, and replace with just `<br'
+              if (addedNode.outerHTML == '<div><br></div>') {
+                addedNode.replaceWith(document.createElement('br'));
+              } // if the added element is a div containing a tag within it (chrome does this when pressing ENTER before a tag)
+              else if (addedNode.nodeType == 1 && addedNode.querySelector(this.settings.classNames.tagSelector)) {
+                let newlineText = document.createTextNode('');
+                if (addedNode.childNodes[0].nodeType == 3 && addedNode.previousSibling.nodeName != 'BR') newlineText = document.createTextNode('\n'); // unwrap the useless div
+                // chrome adds a BR at the end which should be removed
+
+                addedNode.replaceWith(...[newlineText, ...[...addedNode.childNodes].slice(0, -1)]);
+                this.placeCaretAfterNode(newlineText.previousSibling);
+              } // if this is a tag
+              else if (isNodeTag.call(this, addedNode)) {
+                // and it is the first node in a new line
+                if (addedNode.previousSibling && addedNode.previousSibling.nodeName == 'BR') {
+                  // allows placing the caret just before the tag, when the tag is the first node in that line
+                  addedNode.previousSibling.replaceWith('\n\u200B'); // when hitting ENTER for new line just before a tag
+
+                  this.placeCaretAfterNode(addedNode.previousSibling.previousSibling);
+                }
+              }
+            }
+          });
+          record.removedNodes.forEach(removedNode => {
+            // when trying to delete a tag which is in a new line and there's nothing else there (caret is after the tag)
+            if (removedNode && removedNode.nodeName == 'BR' && isNodeTag.call(this, lastInputChild)) {
+              this.removeTags(lastInputChild);
+              this.fixFirefoxLastTagNoCaret();
+            }
+          });
+        }); // get the last child only after the above DOM modifications
+        // check these scenarios:
+        // 1. after a single line, press ENTER once - should add only 1 BR
+        // 2. presss ENTER right before a tag
+        // 3. press enter within a text node before a tag
+
+        var lastInputChild = this.DOM.input.lastChild;
+        if (lastInputChild && lastInputChild.nodeValue == '') lastInputChild.remove(); // make sure the last element is always a BR
+
+        if (!lastInputChild || lastInputChild.nodeName != 'BR') {
+          this.DOM.input.appendChild(document.createElement('br'));
+        }
       }
+
+    }
+  };
+
+  /**
+   * @constructor
+   * @param {Object} input    DOM element
+   * @param {Object} settings settings object
+   */
+
+  function Tagify(input, settings) {
+    if (!input) {
+      console.warn('Tagify:', 'input element not found', input); // return an empty mock of all methods, so the code using tagify will not break
+      // because it might be calling methods even though the input element does not exists
+
+      const mockInstance = new Proxy(this, {
+        get() {
+          return () => mockInstance;
+        }
+
+      });
+      return mockInstance;
     }
-  },
 
-  /**
-   * @param {Node} tagElm the tag element to edit. if nothing specified, use last last
-   */
-  editTag: function editTag() {
-    var _this6 = this;
-
-    var tagElm = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getLastTag();
-
-    var editableElm = tagElm.querySelector('.tagify__tag-text'),
-        tagIdx = this.getNodeIndex(tagElm),
-        tagData = this.value[tagIdx],
-        _CB = this.events.callbacks,
-        that = this,
-        delayed_onEditTagBlur = function delayed_onEditTagBlur() {
-      setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm);
-    };
+    if (input.previousElementSibling && input.previousElementSibling.classList.contains('tagify')) {
+      console.warn('Tagify: ', 'input element is already Tagified', input);
+      return this;
+    }
+
+    extend(this, EventDispatcher(this));
+    this.isFirefox = typeof InstallTrigger !== 'undefined';
+    this.isIE = window.document.documentMode; // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility
+
+    settings = settings || {};
+    this.getPersistedData = getPersistedData(settings.id);
+    this.setPersistedData = setPersistedData(settings.id);
+    this.clearPersistedData = clearPersistedData(settings.id);
+    this.applySettings(input, settings);
+    this.state = {
+      inputText: '',
+      editing: false,
+      actions: {},
+      // UI actions for state-locking
+      mixMode: {},
+      dropdown: {},
+      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
+
+    };
+    this.value = []; // tags' data
+    // events' callbacks references will be stores here, so events could be unbinded
+
+    this.listeners = {};
+    this.DOM = {}; // Store all relevant DOM elements in an Object
+
+    this.build(input);
+    initDropdown.call(this);
+    this.getCSSVars();
+    this.loadOriginalValues();
+    this.events.customBinding.call(this);
+    this.events.binding.call(this);
+    input.autofocus && this.DOM.input.focus();
+  }
+
+  Tagify.prototype = {
+    _dropdown,
+    customEventsList: ['change', 'add', 'remove', 'invalid', 'input', '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'],
+    dataProps: ['__isValid', '__removed', '__originalData', '__originalHTML', '__tagId'],
+
+    // internal-uasge props
+    trim(text) {
+      return this.settings.trim && text && typeof text == "string" ? text.trim() : text;
+    },
+
+    // expose this handy utility function
+    parseHTML,
+    templates,
+
+    parseTemplate(template, data) {
+      template = this.settings.templates[template] || template;
+      return this.parseHTML(template.apply(this, data));
+    },
+
+    set whitelist(arr) {
+      const isArray = arr && Array.isArray(arr);
+      this.settings.whitelist = isArray ? arr : [];
+      this.setPersistedData(isArray ? arr : [], 'whitelist');
+    },
+
+    get whitelist() {
+      return this.settings.whitelist;
+    },
+
+    applySettings(input, settings) {
+      DEFAULTS.templates = this.templates;
+
+      var _s = this.settings = extend({}, DEFAULTS, settings);
+
+      _s.disabled = input.hasAttribute('disabled');
+      _s.readonly = _s.readonly || input.hasAttribute('readonly');
+      _s.placeholder = escapeHTML(input.getAttribute('placeholder') || _s.placeholder || "");
+      _s.required = input.hasAttribute('required');
+
+      for (let name in _s.classNames) Object.defineProperty(_s.classNames, name + "Selector", {
+        get() {
+          return "." + this[name].split(" ")[0];
+        }
+
+      });
+
+      if (this.isIE) _s.autoComplete = false; // IE goes crazy if this isn't false
+
+      ["whitelist", "blacklist"].forEach(name => {
+        var attrVal = input.getAttribute('data-' + name);
+
+        if (attrVal) {
+          attrVal = attrVal.split(_s.delimiters);
+          if (attrVal instanceof Array) _s[name] = attrVal;
+        }
+      }); // backward-compatibility for old version of "autoComplete" setting:
+
+      if ("autoComplete" in settings && !isObject(settings.autoComplete)) {
+        _s.autoComplete = DEFAULTS.autoComplete;
+        _s.autoComplete.enabled = settings.autoComplete;
+      }
+
+      if (_s.mode == 'mix') {
+        _s.autoComplete.rightKey = true;
+        _s.delimiters = settings.delimiters || null; // default dlimiters in mix-mode must be NULL
+        // needed for "filterListItems". This assumes the user might have forgotten to manually
+        // define the same term in "dropdown.searchKeys" as defined in "tagTextProp" setting, so
+        // by automatically adding it, tagify is "helping" out, guessing the intesntions of the developer.
+
+        if (_s.tagTextProp && !_s.dropdown.searchKeys.includes(_s.tagTextProp)) _s.dropdown.searchKeys.push(_s.tagTextProp);
+      }
+
+      if (input.pattern) try {
+        _s.pattern = new RegExp(input.pattern);
+      } catch (e) {} // Convert the "delimiters" setting into a REGEX object
+
+      if (this.settings.delimiters) {
+        try {
+          _s.delimiters = new RegExp(this.settings.delimiters, "g");
+        } catch (e) {}
+      }
+
+      if (_s.disabled) _s.userInput = false;
+      this.TEXTS = _objectSpread2(_objectSpread2({}, TEXTS), _s.texts || {}); // make sure the dropdown will be shown on "focus" and not only after typing something (in "select" mode)
+
+      if (_s.mode == 'select' || !_s.userInput) _s.dropdown.enabled = 0;
+      _s.dropdown.appendTarget = settings.dropdown && settings.dropdown.appendTarget ? settings.dropdown.appendTarget : document.body; // get & merge persisted data with current data
+
+      let persistedWhitelist = this.getPersistedData('whitelist');
+      if (Array.isArray(persistedWhitelist)) this.whitelist = Array.isArray(_s.whitelist) ? concatWithoutDups(_s.whitelist, persistedWhitelist) : persistedWhitelist;
+    },
+
+    /**
+     * Returns a string of HTML element attributes
+     * @param {Object} data [Tag data]
+     */
+    getAttributes(data) {
+      var attrs = this.getCustomAttributes(data),
+          s = '',
+          k;
+
+      for (k in attrs) s += " " + k + (data[k] !== undefined ? `="${attrs[k]}"` : "");
+
+      return s;
+    },
+
+    /**
+     * Returns an object of attributes to be used for the templates
+     */
+    getCustomAttributes(data) {
+      // only items which are objects have properties which can be used as attributes
+      if (!isObject(data)) return '';
+      var output = {},
+          propName;
+
+      for (propName in data) {
+        if (propName.slice(0, 2) != '__' && propName != 'class' && data.hasOwnProperty(propName) && data[propName] !== undefined) output[propName] = escapeHTML(data[propName]);
+      }
+
+      return output;
+    },
+
+    setStateSelection() {
+      var selection = window.getSelection(); // save last selection place to be able to inject anything from outside to that specific place
+
+      var sel = {
+        anchorOffset: selection.anchorOffset,
+        anchorNode: selection.anchorNode,
+        range: selection.getRangeAt && selection.rangeCount && selection.getRangeAt(0)
+      };
+      this.state.selection = sel;
+      return sel;
+    },
+
+    /**
+     * Get the caret position relative to the viewport
+     * https://stackoverflow.com/q/58985076/104380
+     *
+     * @returns {object} left, top distance in pixels
+     */
+    getCaretGlobalPosition() {
+      const sel = document.getSelection();
+
+      if (sel.rangeCount) {
+        const r = sel.getRangeAt(0);
+        const node = r.startContainer;
+        const offset = r.startOffset;
+        let rect, r2;
+
+        if (offset > 0) {
+          r2 = document.createRange();
+          r2.setStart(node, offset - 1);
+          r2.setEnd(node, offset);
+          rect = r2.getBoundingClientRect();
+          return {
+            left: rect.right,
+            top: rect.top,
+            bottom: rect.bottom
+          };
+        }
+
+        if (node.getBoundingClientRect) return node.getBoundingClientRect();
+      }
+
+      return {
+        left: -9999,
+        top: -9999
+      };
+    },
+
+    /**
+     * Get specific CSS variables which are relevant to this script and parse them as needed.
+     * The result is saved on the instance in "this.CSSVars"
+     */
+    getCSSVars() {
+      var compStyle = getComputedStyle(this.DOM.scope, null);
+
+      const getProp = name => compStyle.getPropertyValue('--' + name);
+
+      function seprateUnitFromValue(a) {
+        if (!a) return {};
+        a = a.trim().split(' ')[0];
+        var unit = a.split(/\d+/g).filter(n => n).pop().trim(),
+            value = +a.split(unit).filter(n => n)[0].trim();
+        return {
+          value,
+          unit
+        };
+      }
+
+      this.CSSVars = {
+        tagHideTransition: (({
+          value,
+          unit
+        }) => unit == 's' ? value * 1000 : value)(seprateUnitFromValue(getProp('tag-hide-transition')))
+      };
+    },
+
+    /**
+     * builds the HTML of this component
+     * @param  {Object} input [DOM element which would be "transformed" into "Tags"]
+     */
+    build(input) {
+      var DOM = this.DOM;
+
+      if (this.settings.mixMode.integrated) {
+        DOM.originalInput = null;
+        DOM.scope = input;
+        DOM.input = input;
+      } else {
+        DOM.originalInput = input;
+        DOM.scope = this.parseTemplate('wrapper', [input, this.settings]);
+        DOM.input = DOM.scope.querySelector(this.settings.classNames.inputSelector);
+        input.parentNode.insertBefore(DOM.scope, input);
+      }
+    },
+
+    /**
+     * revert any changes made by this component
+     */
+    destroy() {
+      this.events.unbindGlobal.call(this);
+      this.DOM.scope.parentNode.removeChild(this.DOM.scope);
+      this.dropdown.hide(true);
+      clearTimeout(this.dropdownHide__bindEventsTimeout);
+    },
+
+    /**
+     * if the original input has any values, add them as tags
+     */
+    loadOriginalValues(value) {
+      var lastChild,
+          _s = this.settings; // temporarily block firign the "change" event on the original input unil
+      // this method finish removing current value and adding the new one
+
+      this.state.blockChangeEvent = true;
+
+      if (value === undefined) {
+        const persistedOriginalValue = this.getPersistedData('value'); // if the field already has a field, trust its the desired
+        // one to be rendered and do not use the persisted one
+
+        if (persistedOriginalValue && !this.DOM.originalInput.value) value = persistedOriginalValue;else value = _s.mixMode.integrated ? this.DOM.input.textContent : this.DOM.originalInput.value;
+      }
+
+      this.removeAllTags();
+
+      if (value) {
+        if (_s.mode == 'mix') {
+          this.parseMixTags(this.trim(value));
+          lastChild = this.DOM.input.lastChild;
+          if (!lastChild || lastChild.tagName != 'BR') this.DOM.input.insertAdjacentHTML('beforeend', '<br>');
+        } else {
+          try {
+            if (JSON.parse(value) instanceof Array) value = JSON.parse(value);
+          } catch (err) {}
+
+          this.addTags(value).forEach(tag => tag && tag.classList.add(_s.classNames.tagNoAnimation));
+        }
+      } else this.postUpdate();
+
+      this.state.lastOriginalValueReported = _s.mixMode.integrated ? '' : this.DOM.originalInput.value;
+      this.state.blockChangeEvent = false;
+    },
+
+    cloneEvent(e) {
+      var clonedEvent = {};
 
-    if (!editableElm) {
-      console.warn('Cannot find element in Tag template: ', '.tagify__tag-text');
-      return;
-    }
+      for (var v in e) clonedEvent[v] = e[v];
 
-    if ("editable" in tagData && !tagData.editable) return;
-    tagElm.classList.add('tagify__tag--editable');
-    editableElm.originalValue = editableElm.textContent;
-    editableElm.setAttribute('contenteditable', true);
-    editableElm.addEventListener('blur', delayed_onEditTagBlur);
-    editableElm.addEventListener('input', _CB.onEditTagInput.bind(this, editableElm));
-    editableElm.addEventListener('keydown', function (e) {
-      return _CB.onEditTagkeydown.call(_this6, e);
-    });
-    editableElm.focus();
-    this.setRangeAtStartEnd(false, editableElm);
-    this.state.editing = {
-      scope: tagElm,
-      input: tagElm.querySelector("[contenteditable]")
-    };
-    this.trigger("edit:start", {
-      tag: tagElm,
-      index: tagIdx,
-      data: tagData
-    });
-    return this;
-  },
-  onEditTagDone: function onEditTagDone(tagElm, tagData) {
-    var eventData = {
-      tag: tagElm,
-      index: this.getNodeIndex(tagElm),
-      data: tagData
-    };
-    this.trigger("edit:beforeUpdate", eventData);
-    this.replaceTag(tagElm, tagData);
-    this.trigger("edit:updated", eventData);
-  },
+      return clonedEvent;
+    },
 
-  /**
-   * Exit a tag's edit-mode.
-   * if "tagData" exists, replace the tag element with new data and update Tagify value
-   */
-  replaceTag: function replaceTag(tagElm, tagData) {
-    var _this7 = this;
+    /**
+     * Toogle global loading state on/off
+     * Useful when fetching async whitelist while user is typing
+     * @param {Boolean} isLoading
+     */
+    loading(isLoading) {
+      this.state.isLoading = isLoading; // IE11 doesn't support toggle with second parameter
 
-    var editableElm = tagElm.querySelector('.tagify__tag-text'),
-        clone = editableElm.cloneNode(true),
-        tagElmIdx = this.getNodeIndex(tagElm);
-    if (this.state.editing.locked) return; // when editing a tag and selecting a dropdown suggested item, the state should be "locked"
-    // so "onEditTagBlur" won't run and change the tag also *after* it was just changed.
+      this.DOM.scope.classList[isLoading ? "add" : "remove"](this.settings.classNames.scopeLoading);
+      return this;
+    },
 
-    this.state.editing = {
-      locked: true
-    };
-    setTimeout(function () {
-      return delete _this7.state.editing.locked;
-    }, 500); // update DOM nodes
+    /**
+     * Toogle specieif tag loading state on/off
+     * @param {Boolean} isLoading
+     */
+    tagLoading(tagElm, isLoading) {
+      if (tagElm) // IE11 doesn't support toggle with second parameter
+        tagElm.classList[isLoading ? "add" : "remove"](this.settings.classNames.tagLoading);
+      return this;
+    },
 
-    clone.removeAttribute('contenteditable');
-    tagElm.classList.remove('tagify__tag--editable'); // guarantee to remove all events which were added by the "editTag" method
+    /**
+     * Toggles class on the main tagify container ("scope")
+     * @param {String} className
+     * @param {Boolean} force
+     */
+    toggleClass(className, force) {
+      if (typeof className == 'string') this.DOM.scope.classList.toggle(className, force);
+    },
 
-    editableElm.parentNode.replaceChild(clone, editableElm); // continue only if there was a reason for it
+    toggleFocusClass(force) {
+      this.toggleClass(this.settings.classNames.focus, !!force);
+    },
 
-    if (tagData) {
-      clone.innerHTML = tagData.value;
-      clone.title = tagData.value; // update data
+    triggerChangeEvent,
+    events,
 
-      this.value[tagElmIdx] = tagData;
-      this.update();
-    }
-  },
+    fixFirefoxLastTagNoCaret() {
+      return; // seems to be fixed in newer version of FF, so retiring below code (for now)
+    },
 
-  /** https://stackoverflow.com/a/59156872/104380
-   * @param {Boolean} start indicating where to place it (start or end of the node)
-   * @param {Object}  node  DOM node to place the caret at
-   */
-  setRangeAtStartEnd: function setRangeAtStartEnd(start, node) {
-    node = node || this.DOM.input;
-    node = node.lastChild || node;
-    var sel = document.getSelection();
-
-    if (sel.rangeCount) {
-      ['Start', 'End'].forEach(function (pos) {
-        return sel.getRangeAt(0)["set" + pos](node, start ? 0 : node.length);
-      });
-    }
-  },
+    placeCaretAfterNode(node) {
+      if (!node || !node.parentNode) return;
+      var nextSibling = node.nextSibling,
+          sel = window.getSelection(),
+          range = sel.getRangeAt(0);
 
-  /**
-   * input bridge for accessing & setting
-   * @type {Object}
-   */
-  input: {
-    value: '',
-    set: function set() {
-      var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
-      var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
-      var hideDropdown = this.settings.dropdown.closeOnSelect;
-      this.input.value = s;
-      if (updateDOM) this.DOM.input.innerHTML = s;
-      if (!s && hideDropdown) setTimeout(this.dropdown.hide.bind(this), 20); // setTimeout duration must be HIGER than the dropdown's item "onClick" method's "focus()" event, because the "hide" method re-binds the main events and it will catch the "blur" event and will cause
+      if (sel.rangeCount) {
+        range.setStartAfter(nextSibling || node);
+        range.collapse(true); // range.setEndBefore(nextSibling || node);
 
-      this.input.autocomplete.suggest.call(this);
-      this.input.validate.call(this);
+        sel.removeAllRanges();
+        sel.addRange(range);
+      }
+    },
+
+    insertAfterTag(tagElm, newNode) {
+      newNode = newNode || this.settings.mixMode.insertAfterTag;
+      if (!tagElm || !tagElm.parentNode || !newNode) return;
+      newNode = typeof newNode == 'string' ? document.createTextNode(newNode) : newNode;
+      tagElm.parentNode.insertBefore(newNode, tagElm.nextSibling);
+      return newNode;
     },
 
     /**
-     * Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
+     * Enters a tag into "edit" mode
+     * @param {Node} tagElm the tag element to edit. if nothing specified, use last last
      */
-    validate: function validate() {
-      var isValid = !this.input.value || this.validateTag(this.input.value);
-      if (this.settings.mode == 'select') this.DOM.scope.classList.toggle('tagify--invalid', isValid !== true);else this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true);
-    },
-    // remove any child DOM elements that aren't of type TEXT (like <br>)
-    normalize: function normalize(node) {
-      var clone = node || this.DOM.input,
-          //.cloneNode(true),
-      v = []; // when a text was pasted in FF, the "this.DOM.input" element will have <br> but no newline symbols (\n), and this will
-      // result in tags no being properly created if one wishes to create a separate tag per newline.
-
-      clone.childNodes.forEach(function (n) {
-        return n.nodeType == 3 && v.push(n.nodeValue);
-      });
-      v = v.join("\n");
+    editTag(tagElm, opts) {
+      tagElm = tagElm || this.getLastTag();
+      opts = opts || {};
+      this.dropdown.hide();
+      var _s = this.settings;
+
+      function getEditableElm() {
+        return tagElm.querySelector(_s.classNames.tagTextSelector);
+      }
 
-      try {
-        // "delimiters" might be of a non-regex value, where this will fail ("Tags With Properties" example in demo page):
-        v = v.replace(/(?:\r\n|\r|\n)/g, this.settings.delimiters.source.charAt(0));
-      } catch (err) {}
+      var editableElm = getEditableElm(),
+          tagIdx = this.getNodeIndex(tagElm),
+          tagData = this.tagData(tagElm),
+          _CB = this.events.callbacks,
+          that = this,
+          isValid = true,
+          delayed_onEditTagBlur = function () {
+        setTimeout(() => _CB.onEditTagBlur.call(that, getEditableElm()));
+      };
 
-      v = v.replace(/\s/g, ' ') // replace NBSPs with spaces characters
-      .replace(/^\s+/, ""); // trimLeft
+      if (!editableElm) {
+        console.warn('Cannot find element in Tag template: .', _s.classNames.tagTextSelector);
+        return;
+      }
 
-      return v;
+      if (tagData instanceof Object && "editable" in tagData && !tagData.editable) return;
+      editableElm.setAttribute('contenteditable', true);
+      tagElm.classList.add(_s.classNames.tagEditing); // cache the original data, on the DOM node, before any modification ocurs, for possible revert
+
+      this.tagData(tagElm, {
+        __originalData: extend({}, tagData),
+        __originalHTML: tagElm.innerHTML
+      });
+      editableElm.addEventListener('focus', _CB.onEditTagFocus.bind(this, tagElm));
+      editableElm.addEventListener('blur', delayed_onEditTagBlur);
+      editableElm.addEventListener('input', _CB.onEditTagInput.bind(this, editableElm));
+      editableElm.addEventListener('keydown', e => _CB.onEditTagkeydown.call(this, e, tagElm));
+      editableElm.focus();
+      this.setRangeAtStartEnd(false, editableElm);
+      if (!opts.skipValidation) isValid = this.editTagToggleValidity(tagElm);
+      editableElm.originalIsValid = isValid;
+      this.trigger("edit:start", {
+        tag: tagElm,
+        index: tagIdx,
+        data: tagData,
+        isValid
+      });
+      return this;
     },
 
     /**
-     * suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
-     * @param  {String} s [description]
+     * If a tag is invalid, for any reason, set its class to as "not allowed" (see defaults file)
+     * @param {Node} tagElm required
+     * @param {Object} tagData optional
+     * @returns true if valid, a string (reason) if not
      */
-    autocomplete: {
-      suggest: function suggest(data) {
-        if (!this.settings.autoComplete.enabled) return;
-        data = data || {};
-        if (typeof data == 'string') data = {
-          value: data
-        };
-        var suggestedText = data.value || '',
-            suggestionStart = suggestedText.substr(0, this.input.value.length).toLowerCase(),
-            suggestionTrimmed = suggestedText.substring(this.input.value.length);
+    editTagToggleValidity(tagElm, tagData) {
+      var tagData = tagData || this.tagData(tagElm),
+          isValid;
 
-        if (!suggestedText || !this.input.value || suggestionStart != this.input.value.toLowerCase()) {
-          this.DOM.input.removeAttribute("data-suggest");
-          delete this.state.inputSuggestion;
-        } else {
-          this.DOM.input.setAttribute("data-suggest", suggestionTrimmed);
-          this.state.inputSuggestion = data;
-        }
-      },
-
-      /**
-       * sets the suggested text as the input's value & cleanup the suggestion autocomplete.
-       * @param {String} s [text]
-       */
-      set: function set(s) {
-        var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
-            suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null);
-
-        if (suggestion) {
-          if (this.settings.mode == 'mix') {
-            this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix + suggestion));
-          } else {
-            this.input.set.call(this, suggestion);
-            this.setRangeAtStartEnd();
-          }
+      if (!tagData) {
+        console.warn("tag has no data: ", tagElm, tagData);
+        return;
+      }
 
-          this.input.autocomplete.suggest.call(this);
-          this.dropdown.hide.call(this);
-          return true;
-        }
+      isValid = !("__isValid" in tagData) || tagData.__isValid === true;
 
-        return false;
+      if (!isValid) {
+        this.removeTagsFromValue(tagElm);
       }
-    }
-  },
-  getNodeIndex: function getNodeIndex(node) {
-    var index = 0;
-    if (node) while (node = node.previousElementSibling) {
-      index++;
-    }
-    return index;
-  },
-  getTagElms: function getTagElms() {
-    return this.DOM.scope.querySelectorAll('.tagify__tag');
-  },
-  getLastTag: function getLastTag() {
-    var lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide):not([readonly])');
-    return lastTag[lastTag.length - 1];
-  },
 
-  /**
-   * Searches if any tag with a certain value already exis
-   * @param  {String/Object} v [text value / tag data object]
-   * @return {Boolean}
-   */
-  isTagDuplicate: function isTagDuplicate(v) {
-    var _this8 = this;
+      this.update(); //this.validateTag(tagData);
 
-    // duplications are irrelevant for this scenario
-    if (this.settings.mode == 'select') return false;
-    return this.value.some(function (item) {
-      return _this8.isObject(v) ? JSON.stringify(item).toLowerCase() === JSON.stringify(v).toLowerCase() : v.trim().toLowerCase() === item.value.toLowerCase();
-    });
-  },
-  getTagIndexByValue: function getTagIndexByValue(value) {
-    var result = [];
-    this.getTagElms().forEach(function (tagElm, i) {
-      if (tagElm.textContent.trim().toLowerCase() == value.toLowerCase()) result.push(i);
-    });
-    return result;
-  },
-  getTagElmByValue: function getTagElmByValue(value) {
-    var tagIdx = this.getTagIndexByValue(value)[0];
-    return this.getTagElms()[tagIdx];
-  },
+      tagElm.classList.toggle(this.settings.classNames.tagNotAllowed, !isValid);
+      return tagData.__isValid;
+    },
 
-  /**
-   * Mark a tag element by its value
-   * @param  {String|Number} value  [text value to search for]
-   * @param  {Object}          tagElm [a specific "tag" element to compare to the other tag elements siblings]
-   * @return {boolean}                [found / not found]
-   */
-  markTagByValue: function markTagByValue(value, tagElm) {
-    tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined
+    onEditTagDone(tagElm, tagData) {
+      tagElm = tagElm || this.state.editing.scope;
+      tagData = tagData || {};
+      var eventData = {
+        tag: tagElm,
+        index: this.getNodeIndex(tagElm),
+        previousData: this.tagData(tagElm),
+        data: tagData
+      };
+      this.trigger("edit:beforeUpdate", eventData, {
+        cloneData: false
+      });
+      this.state.editing = false;
+      delete tagData.__originalData;
+      delete tagData.__originalHTML;
 
-    if (tagElm) {
-      tagElm.classList.add('tagify--mark'); //   setTimeout(() => { tagElm.classList.remove('tagify--mark') }, 100);
+      if (tagElm && tagData[this.settings.tagTextProp]) {
+        tagElm = this.replaceTag(tagElm, tagData);
+        this.editTagToggleValidity(tagElm, tagData);
+        if (this.settings.a11y.focusableTags) tagElm.focus();else // place caret after edited tag
+          this.placeCaretAfterNode(tagElm.previousSibling);
+      } else if (tagElm) this.removeTags(tagElm);
 
-      return tagElm;
-    }
+      this.trigger("edit:updated", eventData);
+      this.dropdown.hide(); // check if any of the current tags which might have been marked as "duplicate" should be now un-marked
 
-    return false;
-  },
+      if (this.settings.keepInvalidTags) this.reCheckInvalidTags();
+    },
 
-  /**
-   * make sure the tag, or words in it, is not in the blacklist
-   */
-  isTagBlacklisted: function isTagBlacklisted(v) {
-    v = v.toLowerCase().trim();
-    return this.settings.blacklist.filter(function (x) {
-      return v == x.toLowerCase();
-    }).length;
-  },
+    /**
+     * Replaces an exisitng tag with a new one. Used for updating a tag's data
+     * @param {Object} tagElm  [DOM node to replace]
+     * @param {Object} tagData [data to create new tag from]
+     */
+    replaceTag(tagElm, tagData) {
+      if (!tagData || !tagData.value) tagData = tagElm.__tagifyTagData; // if tag is invalid, make the according changes in the newly created element
 
-  /**
-   * make sure the tag, or words in it, is not in the blacklist
-   */
-  isTagWhitelisted: function isTagWhitelisted(v) {
-    return this.settings.whitelist.some(function (item) {
-      return typeof v == 'string' ? v.trim().toLowerCase() === (item.value || item).toLowerCase() : JSON.stringify(item).toLowerCase() === JSON.stringify(v).toLowerCase();
-    });
-  },
+      if (tagData.__isValid && tagData.__isValid != true) extend(tagData, this.getInvalidTagAttrs(tagData, tagData.__isValid));
+      var newTagElm = this.createTagElem(tagData); // update DOM
 
-  /**
-   * validate a tag object BEFORE the actual tag will be created & appeneded
-   * @param  {String} s
-   * @return {Boolean/String}  ["true" if validation has passed, String for a fail]
-   */
-  validateTag: function validateTag(s) {
-    var value = s.trim(),
-        _s = this.settings,
-        result = true; // check for empty value
-
-    if (!value) result = this.TEXTS.empty; // check if pattern should be used and if so, use it to test the value
-    else if (_s.pattern && !_s.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate
-      else if (!_s.duplicates && this.isTagDuplicate(value)) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || _s.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed;
-    return result;
-  },
-  hasMaxTags: function hasMaxTags() {
-    if (this.value.length >= this.settings.maxTags) return this.TEXTS.exceed;
-    return false;
-  },
+      tagElm.parentNode.replaceChild(newTagElm, tagElm);
+      this.updateValueByDOMTags();
+      return newTagElm;
+    },
 
-  /**
-   * pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words
-   * so each item should be iterated on and a tag created for.
-   * @return {Array} [Array of Objects]
-   */
-  normalizeTags: function normalizeTags(tagsItems) {
-    var _this$settings = this.settings,
-        whitelist = _this$settings.whitelist,
-        delimiters = _this$settings.delimiters,
-        mode = _this$settings.mode,
-        whitelistWithProps = whitelist ? whitelist[0] instanceof Object : false,
-        isArray = tagsItems instanceof Array,
-        isCollection = isArray && tagsItems[0] instanceof Object && "value" in tagsItems[0],
-        temp = [],
-        mapStringToCollection = function mapStringToCollection(s) {
-      return s.split(delimiters).filter(function (n) {
-        return n;
-      }).map(function (v) {
-        return {
-          value: v.trim()
-        };
+    /**
+     * update "value" (Array of Objects) by traversing all valid tags
+     */
+    updateValueByDOMTags() {
+      this.value.length = 0;
+      [].forEach.call(this.getTagElms(), node => {
+        if (node.classList.contains(this.settings.classNames.tagNotAllowed.split(' ')[0])) return;
+        this.value.push(this.tagData(node));
       });
-    }; // no need to continue if "tagsItems" is an Array of Objects
+      this.update();
+    },
+
+    /** https://stackoverflow.com/a/59156872/104380
+     * @param {Boolean} start indicating where to place it (start or end of the node)
+     * @param {Object}  node  DOM node to place the caret at
+     */
+    setRangeAtStartEnd(start, node) {
+      start = typeof start == 'number' ? start : !!start;
+      node = node || this.DOM.input;
+      node = node.lastChild || node;
+      var sel = document.getSelection();
 
+      try {
+        if (sel.rangeCount >= 1) {
+          ['Start', 'End'].forEach(pos => sel.getRangeAt(0)["set" + pos](node, start ? start : node.length));
+        }
+      } catch (err) {// console.warn("Tagify: ", err)
+      }
+    },
 
-    if (isCollection) {
-      var _ref2;
+    /**
+     * injects nodes/text at caret position, which is saved on the "state" when "blur" event gets triggered
+     * @param {Node} injectedNode [the node to inject at the caret position]
+     * @param {Object} selection [optional range Object. must have "anchorNode" & "anchorOffset"]
+     */
+    injectAtCaret(injectedNode, range) {
+      range = range || this.state.selection.range;
+      if (!range) return;
+      if (typeof injectedNode == 'string') injectedNode = document.createTextNode(injectedNode);
+      range.deleteContents();
+      range.insertNode(injectedNode);
+      this.setRangeAtStartEnd(false, injectedNode);
+      this.updateValueByDOMTags(); // updates internal "this.value"
 
-      // iterate the collection items and check for values that can be splitted into multiple tags
-      tagsItems = (_ref2 = []).concat.apply(_ref2, _toConsumableArray(tagsItems.map(function (item) {
-        return mapStringToCollection(item.value).map(function (newItem) {
-          return _objectSpread({}, item, {}, newItem);
-        });
-      })));
-      return tagsItems;
-    }
+      this.update(); // updates original input/textarea
 
-    if (typeof tagsItems == 'number') tagsItems = tagsItems.toString(); // if the value is a "simple" String, ex: "aaa, bbb, ccc"
+      return this;
+    },
 
-    if (typeof tagsItems == 'string') {
-      if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones)
+    /**
+     * input bridge for accessing & setting
+     * @type {Object}
+     */
+    input: {
+      set(s = '', updateDOM = true) {
+        var hideDropdown = this.settings.dropdown.closeOnSelect;
+        this.state.inputText = s;
+        if (updateDOM) this.DOM.input.innerHTML = escapeHTML("" + s);
+        if (!s && hideDropdown) this.dropdown.hide.bind(this);
+        this.input.autocomplete.suggest.call(this);
+        this.input.validate.call(this);
+      },
 
-      tagsItems = mapStringToCollection(tagsItems);
-    } else if (isArray) {
-      var _ref3;
+      raw() {
+        return this.DOM.input.textContent;
+      },
 
-      tagsItems = (_ref3 = []).concat.apply(_ref3, _toConsumableArray(tagsItems.map(function (item) {
-        return mapStringToCollection(item);
-      })));
-    } // search if the tag exists in the whitelist as an Object (has props),
-    // to be able to use its properties
+      /**
+       * Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
+       */
+      validate() {
+        var isValid = !this.state.inputText || this.validateTag({
+          value: this.state.inputText
+        }) === true;
+        this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid, !isValid);
+        return isValid;
+      },
 
+      // remove any child DOM elements that aren't of type TEXT (like <br>)
+      normalize(node) {
+        var clone = node || this.DOM.input,
+            //.cloneNode(true),
+        v = []; // when a text was pasted in FF, the "this.DOM.input" element will have <br> but no newline symbols (\n), and this will
+        // result in tags not being properly created if one wishes to create a separate tag per newline.
 
-    if (whitelistWithProps) {
-      tagsItems.forEach(function (item) {
-        // the "value" prop should preferably be unique
-        var matchObj = whitelist.filter(function (WL_item) {
-          return WL_item.value.toLowerCase() == item.value.toLowerCase();
-        });
+        clone.childNodes.forEach(n => n.nodeType == 3 && v.push(n.nodeValue));
+        v = v.join("\n");
 
-        if (matchObj[0]) {
-          temp.push(matchObj[0]); // set the Array (with the found Object) as the new value
-        } else if (mode != 'mix') temp.push(item);
-      });
-      tagsItems = temp;
-    }
+        try {
+          // "delimiters" might be of a non-regex value, where this will fail ("Tags With Properties" example in demo page):
+          v = v.replace(/(?:\r\n|\r|\n)/g, this.settings.delimiters.source.charAt(0));
+        } catch (err) {}
 
-    return tagsItems;
-  },
+        v = v.replace(/\s/g, ' '); // replace NBSPs with spaces characters
 
-  /**
-   * Used to parse the initial value of a textarea (or input) element and gemerate mixed text w/ tags
-   * https://stackoverflow.com/a/57598892/104380
-   * @param {String} s
-   */
-  parseMixTags: function parseMixTags(s) {
-    var _this9 = this;
-
-    var _this$settings2 = this.settings,
-        mixTagsInterpolator = _this$settings2.mixTagsInterpolator,
-        duplicates = _this$settings2.duplicates,
-        transformTag = _this$settings2.transformTag,
-        enforceWhitelist = _this$settings2.enforceWhitelist;
-    s = s.split(mixTagsInterpolator[0]).map(function (s1, i) {
-      var s2 = s1.split(mixTagsInterpolator[1]),
-          preInterpolated = s2[0],
-          tagData,
-          tagElm;
+        if (this.settings.trim) v = v.replace(/^\s+/, ''); // trimLeft
 
-      try {
-        tagData = JSON.parse(preInterpolated);
-      } catch (err) {
-        tagData = _this9.normalizeTags(preInterpolated)[0]; //{value:preInterpolated}
-      }
+        return v;
+      },
 
-      if (s2.length > 1 && (!enforceWhitelist || _this9.isTagWhitelisted(tagData.value)) && !(!duplicates && _this9.isTagDuplicate(tagData))) {
-        transformTag.call(_this9, tagData);
-        tagElm = _this9.createTagElem(tagData);
-        s2[0] = tagElm.outerHTML; //+ "&#8288;"  // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag)
+      /**
+       * suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
+       * @param  {String} s [description]
+       */
+      autocomplete: {
+        suggest(data) {
+          if (!this.settings.autoComplete.enabled) return;
+          data = data || {};
+          if (typeof data == 'string') data = {
+            value: data
+          };
+          var suggestedText = data.value ? '' + data.value : '',
+              suggestionStart = suggestedText.substr(0, this.state.inputText.length).toLowerCase(),
+              suggestionTrimmed = suggestedText.substring(this.state.inputText.length);
+
+          if (!suggestedText || !this.state.inputText || suggestionStart != this.state.inputText.toLowerCase()) {
+            this.DOM.input.removeAttribute("data-suggest");
+            delete this.state.inputSuggestion;
+          } else {
+            this.DOM.input.setAttribute("data-suggest", suggestionTrimmed);
+            this.state.inputSuggestion = data;
+          }
+        },
 
-        _this9.value.push(tagData);
-      } else if (s1) return i ? mixTagsInterpolator[0] + s1 : s1;
+        /**
+         * sets the suggested text as the input's value & cleanup the suggestion autocomplete.
+         * @param {String} s [text]
+         */
+        set(s) {
+          var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
+              suggestion = s || (dataSuggest ? this.state.inputText + dataSuggest : null);
+
+          if (suggestion) {
+            if (this.settings.mode == 'mix') {
+              this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix + suggestion));
+            } else {
+              this.input.set.call(this, suggestion);
+              this.setRangeAtStartEnd();
+            }
 
-      return s2.join('');
-    }).join('');
-    this.DOM.input.innerHTML = s;
-    this.DOM.input.appendChild(document.createTextNode(''));
-    this.update();
-    return s;
-  },
+            this.input.autocomplete.suggest.call(this);
+            this.dropdown.hide();
+            return true;
+          }
 
-  /**
-   * For mixed-mode: replaces a text starting with a prefix with a wrapper element (tag or something)
-   * First there *has* to be a "this.state.tag" which is a string that was just typed and is staring with a prefix
-   */
-  replaceTextWithNode: function replaceTextWithNode(wrapperElm, tagString) {
-    if (!this.state.tag && !tagString) return;
-    tagString = tagString || this.state.tag.prefix + this.state.tag.value;
-    var idx,
-        replacedNode,
-        selection = window.getSelection(),
-        nodeAtCaret = selection.anchorNode; // ex. replace #ba with the tag "bart" where "|" is where the caret is:
-    // start with: "#ba #ba| #ba"
-    // split the text node at the index of the caret
-
-    nodeAtCaret.splitText(selection.anchorOffset); // "#ba #ba"
-    // get index of last occurence of "#ba"
-
-    idx = nodeAtCaret.nodeValue.lastIndexOf(tagString);
-    replacedNode = nodeAtCaret.splitText(idx); // clean up the tag's string and put tag element instead
-
-    replacedNode.nodeValue = replacedNode.nodeValue.replace(tagString, '');
-    nodeAtCaret.parentNode.insertBefore(wrapperElm, replacedNode);
-    this.DOM.input.normalize();
-    return replacedNode;
-  },
+          return false;
+        }
 
-  /**
-   * For selecting a single option (not used for multiple tags)
-   * @param {Object} tagElm   Tag DOM node
-   * @param {Object} tagData  Tag data
-   */
-  selectTag: function selectTag(tagElm, tagData) {
-    this.input.set.call(this, tagData.value, true);
-    setTimeout(this.setRangeAtStartEnd.bind(this));
-    if (this.getLastTag()) this.replaceTag(this.getLastTag(), tagData);else this.appendTag(tagElm);
-    this.value[0] = tagData;
-    this.trigger('add', {
-      tag: tagElm,
-      data: tagData
-    });
-    this.update();
-    return [tagElm];
-  },
+      }
+    },
 
-  /**
-   * add an empty "tag" element in an editable state
-   */
-  addEmptyTag: function addEmptyTag() {
-    var tagData = {
-      value: ""
+    /**
+     * returns the index of the the tagData within the "this.value" array collection.
+     * since values should be unique, it is suffice to only search by "value" property
+     * @param {Object} tagData
+     */
+    getTagIdx(tagData) {
+      return this.value.findIndex(item => item.__tagId == (tagData || {}).__tagId);
     },
-        tagElm = this.createTagElem(tagData); // add the tag to the component's DOM
 
-    this.appendTag(tagElm);
-    this.value.push(tagData);
-    this.update();
-    this.editTag(tagElm);
-  },
+    getNodeIndex(node) {
+      var index = 0;
+      if (node) while (node = node.previousElementSibling) index++;
+      return index;
+    },
 
-  /**
-   * add a "tag" element to the "tags" component
-   * @param {String/Array} tagsItems   [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]
-   * @param {Boolean}      clearInput  [flag if the input's value should be cleared after adding tags]
-   * @param {Boolean}      skipInvalid [do not add, mark & remove invalid tags]
-   * @return {Array} Array of DOM elements (tags)
-   */
-  addTags: function addTags(tagsItems, clearInput) {
-    var _this10 = this;
+    getTagElms(...classess) {
+      var classname = '.' + [...this.settings.classNames.tag.split(' '), ...classess].join('.');
+      return [].slice.call(this.DOM.scope.querySelectorAll(classname)); // convert nodeList to Array - https://stackoverflow.com/a/3199627/104380
+    },
 
-    var skipInvalid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.settings.skipInvalid;
-    var tagElems = [],
-        tagElm,
-        _s = this.settings;
+    /**
+     * gets the last non-readonly, not-in-the-proccess-of-removal tag
+     */
+    getLastTag() {
+      var lastTag = this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);
+      return lastTag[lastTag.length - 1];
+    },
 
-    if (!tagsItems || tagsItems.length == 0) {
-      // is mode is "select" clean all tags
-      if (_s.mode == 'select') this.removeAllTags();
-      return tagElems;
-    } // converts Array/String/Object to an Array of Objects
+    /** Setter/Getter
+     * Each tag DOM node contains a custom property called "__tagifyTagData" which hosts its data
+     * @param {Node}   tagElm
+     * @param {Object} data
+     */
+    tagData(tagElm, data, override) {
+      if (!tagElm) {
+        console.warn("tag element doesn't exist", tagElm, data);
+        return data;
+      }
 
+      if (data) tagElm.__tagifyTagData = override ? data : extend({}, tagElm.__tagifyTagData || {}, data);
+      return tagElm.__tagifyTagData;
+    },
 
-    tagsItems = this.normalizeTags(tagsItems); // if in edit-mode, do not continue but instead replace the tag's text
+    /**
+     * Searches if any tag with a certain value already exis
+     * @param  {String/Object} value [text value / tag data object]
+     * @param  {Boolean} caseSensitive
+     * @return {Number}
+     */
+    isTagDuplicate(value, caseSensitive) {
+      var dupsCount,
+          _s = this.settings; // duplications are irrelevant for this scenario
 
-    if (this.state.editing.scope) {
-      return this.onEditTagDone(this.state.editing.scope, tagsItems[0]);
-    }
+      if (_s.mode == 'select') return false;
+      dupsCount = this.value.reduce((acc, item) => sameStr(this.trim("" + value), item.value, caseSensitive || _s.dropdown.caseSensitive) ? acc + 1 : acc, 0);
+      return dupsCount;
+    },
 
-    if (_s.mode == 'mix') {
-      _s.transformTag.call(this, tagsItems[0]);
+    getTagIndexByValue(value) {
+      var indices = [];
+      this.getTagElms().forEach((tagElm, i) => {
+        if (sameStr(this.trim(tagElm.textContent), value, this.settings.dropdown.caseSensitive)) indices.push(i);
+      });
+      return indices;
+    },
 
-      tagElm = this.createTagElem(tagsItems[0]); // insert the new tag to the END if "addTags" was called from outside
+    getTagElmByValue(value) {
+      var tagIdx = this.getTagIndexByValue(value)[0];
+      return this.getTagElms()[tagIdx];
+    },
 
-      if (!this.replaceTextWithNode(tagElm)) {
-        this.DOM.input.appendChild(tagElm);
-      } // fixes a firefox bug where if the last child of the input is a tag and not a text, the input cannot get focus (by Tab key)
+    /**
+     * Temporarily marks a tag element (by value or Node argument)
+     * @param  {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings]
+     */
+    flashTag(tagElm) {
+      if (tagElm) {
+        tagElm.classList.add(this.settings.classNames.tagFlash);
+        setTimeout(() => {
+          tagElm.classList.remove(this.settings.classNames.tagFlash);
+        }, 100);
+      }
+    },
 
+    /**
+     * checks if text is in the blacklist
+     */
+    isTagBlacklisted(v) {
+      v = this.trim(v.toLowerCase());
+      return this.settings.blacklist.filter(x => ("" + x).toLowerCase() == v).length;
+    },
 
-      this.DOM.input.appendChild(document.createTextNode(''));
-      tagsItems[0].prefix = tagsItems[0].prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source || _s.pattern)[0];
-      this.value.push(tagsItems[0]);
-      this.update();
-      this.state.tag = null;
-      this.trigger('add', this.extend({}, {
-        tag: tagElm
-      }, {
-        data: tagsItems[0]
-      })); // fixes a firefox bug where if the last child of the input is a tag and not a text, the input cannot get focus (by Tab key)
+    /**
+     * checks if text is in the whitelist
+     */
+    isTagWhitelisted(v) {
+      return !!this.getWhitelistItem(v);
+      /*
+      return this.settings.whitelist.some(item =>
+          typeof v == 'string'
+              ? sameStr(this.trim(v), (item.value || item))
+              : sameStr(JSON.stringify(item), JSON.stringify(v))
+      )
+      */
+    },
 
-      this.DOM.input.appendChild(document.createTextNode(''));
-      return tagElm;
-    }
+    /**
+     * Returns the first whitelist item matched, by value (if match found)
+     * @param {String} value [text to match by]
+     */
+    getWhitelistItem(value, prop, whitelist) {
+      var result,
+          prop = prop || 'value',
+          _s = this.settings,
+          whitelist = whitelist || _s.whitelist;
+      whitelist.some(_wi => {
+        var _wiv = typeof _wi == 'string' ? _wi : _wi[prop] || _wi.value,
+            isSameStr = sameStr(_wiv, value, _s.dropdown.caseSensitive, _s.trim);
+
+        if (isSameStr) {
+          result = typeof _wi == 'string' ? {
+            value: _wi
+          } : _wi;
+          return true;
+        }
+      }); // first iterate the whitelist, try find maches by "value" and if that fails
+      // and a "tagTextProp" is set to be other than "value", try that also
 
-    if (_s.mode == 'select') clearInput = false;
-    this.DOM.input.removeAttribute('style');
-    tagsItems.forEach(function (tagData) {
-      var tagValidation,
-          tagElm,
-          tagElmParams = {}; // shallow-clone tagData so later modifications will not apply to the source
+      if (!result && prop == 'value' && _s.tagTextProp != 'value') {
+        // if found, adds the first which matches
+        result = this.getWhitelistItem(value, _s.tagTextProp, whitelist);
+      }
 
-      tagData = Object.assign({}, tagData);
+      return result;
+    },
 
-      _s.transformTag.call(_this10, tagData); ///////////////// ( validation )//////////////////////
+    /**
+     * validate a tag object BEFORE the actual tag will be created & appeneded
+     * @param  {String} s
+     * @param  {String} uid      [unique ID, to not inclue own tag when cheking for duplicates]
+     * @return {Boolean/String}  ["true" if validation has passed, String for a fail]
+     */
+    validateTag(tagData) {
+      var _s = this.settings,
+          // when validating a tag in edit-mode, need to take "tagTextProp" into consideration
+      prop = "value" in tagData ? "value" : _s.tagTextProp,
+          v = this.trim(tagData[prop] + ""); // check for definitive empty value
 
+      if (!(tagData[prop] + "").trim()) return this.TEXTS.empty; // check if pattern should be used and if so, use it to test the value
 
-      tagValidation = _this10.hasMaxTags() || _this10.validateTag(tagData.value);
+      if (_s.pattern && _s.pattern instanceof RegExp && !_s.pattern.test(v)) return this.TEXTS.pattern; // check for duplicates
 
-      if (tagValidation !== true) {
-        if (skipInvalid) return;
-        tagElmParams["aria-invalid"] = true;
-        tagElmParams["class"] = (tagData["class"] || '') + ' tagify--notAllowed';
-        tagElmParams.title = tagValidation;
+      if (!_s.duplicates && this.isTagDuplicate(v, this.state.editing)) return this.TEXTS.duplicate;
+      if (this.isTagBlacklisted(v) || _s.enforceWhitelist && !this.isTagWhitelisted(v)) return this.TEXTS.notAllowed;
+      if (_s.validate) return _s.validate(tagData);
+      return true;
+    },
 
-        _this10.markTagByValue(tagData.value);
-      } /////////////////////////////////////////////////////
-      // add accessibility attributes
+    getInvalidTagAttrs(tagData, validation) {
+      return {
+        "aria-invalid": true,
+        "class": `${tagData.class || ''} ${this.settings.classNames.tagNotAllowed}`.trim(),
+        "title": validation
+      };
+    },
 
+    hasMaxTags() {
+      return this.value.length >= this.settings.maxTags ? this.TEXTS.exceed : false;
+    },
 
-      tagElmParams.role = "tag";
-      if (tagData.readonly) tagElmParams["aria-readonly"] = true; // Create tag HTML element
+    setReadonly(toggle, attrribute) {
+      var _s = this.settings;
+      document.activeElement.blur(); // exit possible edit-mode
 
-      tagElm = _this10.createTagElem(_this10.extend({}, tagData, tagElmParams));
-      tagElems.push(tagElm); // mode-select overrides
+      _s[attrribute || 'readonly'] = toggle;
+      this.DOM.scope[(toggle ? 'set' : 'remove') + 'Attribute'](attrribute || 'readonly', true);
 
-      if (_s.mode == 'select') {
-        return _this10.selectTag(tagElm, tagData);
-      } // add the tag to the component's DOM
+      if (_s.mode == 'mix') {
+        this.setContentEditable(!toggle);
+      }
+    },
 
+    setContentEditable(state) {
+      if (!this.settings.readonly && this.settings.userInput) this.DOM.input.contentEditable = state;
+    },
 
-      _this10.appendTag(tagElm);
+    setDisabled(isDisabled) {
+      this.setReadonly(isDisabled, 'disabled');
+    },
 
-      if (tagValidation === true) {
-        // update state
-        _this10.value.push(tagData);
+    /**
+     * pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words
+     * so each item should be iterated on and a tag created for.
+     * @return {Array} [Array of Objects]
+     */
+    normalizeTags(tagsItems) {
+      var _this$settings = this.settings,
+          whitelist = _this$settings.whitelist,
+          delimiters = _this$settings.delimiters,
+          mode = _this$settings.mode,
+          tagTextProp = _this$settings.tagTextProp;
+          _this$settings.enforceWhitelist;
+          var whitelistMatches = [],
+          whitelistWithProps = whitelist ? whitelist[0] instanceof Object : false,
+          isArray = tagsItems instanceof Array,
+          mapStringToCollection = s => (s + "").split(delimiters).filter(n => n).map(v => ({
+        [tagTextProp]: this.trim(v),
+        value: this.trim(v)
+      }));
+
+      if (typeof tagsItems == 'number') tagsItems = tagsItems.toString(); // if the argument is a "simple" String, ex: "aaa, bbb, ccc"
+
+      if (typeof tagsItems == 'string') {
+        if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones)
+
+        tagsItems = mapStringToCollection(tagsItems);
+      } // is is an Array of Strings, convert to an Array of Objects
+      else if (isArray) {
+        // flatten the 2D array
+        tagsItems = [].concat(...tagsItems.map(item => item.value ? item // mapStringToCollection(item.value).map(newItem => ({...item,...newItem}))
+        : mapStringToCollection(item)));
+      } // search if the tag exists in the whitelist as an Object (has props),
+      // to be able to use its properties
+
+
+      if (whitelistWithProps) {
+        tagsItems.forEach(item => {
+          var whitelistMatchesValues = whitelistMatches.map(a => a.value); // if suggestions are shown, they are already filtered, so it's easier to use them,
+          // because the whitelist might also include items which have already been added
+
+          var filteredList = this.dropdown.filterListItems.call(this, item[tagTextProp], {
+            exact: true
+          });
+          if (!this.settings.duplicates) // also filter out items which have already been matched in previous iterations
+            filteredList = filteredList.filter(filteredItem => !whitelistMatchesValues.includes(filteredItem.value)); // get the best match out of list of possible matches.
+          // if there was a single item in the filtered list, use that one
 
-        _this10.update();
+          var matchObj = filteredList.length > 1 ? this.getWhitelistItem(item[tagTextProp], tagTextProp, filteredList) : filteredList[0];
 
-        _this10.trigger('add', {
-          tag: tagElm,
-          index: _this10.value.length - 1,
-          data: tagData
-        });
-      } else {
-        _this10.trigger("invalid", {
-          data: tagData,
-          index: _this10.value.length,
-          tag: tagElm,
-          message: tagValidation
+          if (matchObj && matchObj instanceof Object) {
+            whitelistMatches.push(matchObj); // set the Array (with the found Object) as the new value
+          } else if (mode != 'mix') {
+            if (item.value == undefined) item.value = item[tagTextProp];
+            whitelistMatches.push(item);
+          }
         });
-
-        if (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false")
-          setTimeout(function () {
-            return _this10.removeTag(tagElm, true);
-          }, 1000);
+        if (whitelistMatches.length) tagsItems = whitelistMatches;
       }
 
-      _this10.dropdown.position.call(_this10); // reposition the dropdown because the just-added tag might cause a new-line
+      return tagsItems;
+    },
 
-    });
+    /**
+     * Parse the initial value of a textarea (or input) element and generate mixed text w/ tags
+     * https://stackoverflow.com/a/57598892/104380
+     * @param {String} s
+     */
+    parseMixTags(s) {
+      var _this$settings2 = this.settings,
+          mixTagsInterpolator = _this$settings2.mixTagsInterpolator,
+          duplicates = _this$settings2.duplicates,
+          transformTag = _this$settings2.transformTag,
+          enforceWhitelist = _this$settings2.enforceWhitelist,
+          maxTags = _this$settings2.maxTags,
+          tagTextProp = _this$settings2.tagTextProp,
+          tagsDataSet = [];
+      s = s.split(mixTagsInterpolator[0]).map((s1, i) => {
+        var s2 = s1.split(mixTagsInterpolator[1]),
+            preInterpolated = s2[0],
+            maxTagsReached = tagsDataSet.length == maxTags,
+            textProp,
+            tagData,
+            tagElm;
 
-    if (tagsItems.length && clearInput) {
-      this.input.set.call(this);
-    }
+        try {
+          // skip numbers and go straight to the "catch" statement
+          if (preInterpolated == +preInterpolated) throw Error;
+          tagData = JSON.parse(preInterpolated);
+        } catch (err) {
+          tagData = this.normalizeTags(preInterpolated)[0] || {
+            value: preInterpolated
+          };
+        }
 
-    this.dropdown.refilter.call(this);
-    return tagElems;
-  },
+        transformTag.call(this, tagData);
 
-  /**
-   * appened (validated) tag to the component's DOM scope
-   */
-  appendTag: function appendTag(tagElm) {
-    var insertBeforeNode = this.DOM.scope.lastElementChild;
-    if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm);
-  },
+        if (!maxTagsReached && s2.length > 1 && (!enforceWhitelist || this.isTagWhitelisted(tagData.value)) && !(!duplicates && this.isTagDuplicate(tagData.value))) {
+          // in case "tagTextProp" setting is set to other than "value" and this tag does not have this prop
+          textProp = tagData[tagTextProp] ? tagTextProp : 'value';
+          tagData[textProp] = this.trim(tagData[textProp]);
+          tagElm = this.createTagElem(tagData);
+          tagsDataSet.push(tagData);
+          tagElm.classList.add(this.settings.classNames.tagNoAnimation);
+          s2[0] = tagElm.outerHTML; //+ "&#8288;"  // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag)
 
-  /**
-   * Removed new lines and irrelevant spaces which might affect layout, and are better gone
-   * @param {string} s [HTML string]
-   */
-  minify: function minify(s) {
-    return s ? s.replace(/\>[\r\n ]+\</g, "><").replace(/(<.*?>)|\s+/g, function (m, $1) {
-      return $1 ? $1 : ' ';
-    }) // https://stackoverflow.com/a/44841484/104380
-    : "";
-  },
+          this.value.push(tagData);
+        } else if (s1) return i ? mixTagsInterpolator[0] + s1 : s1;
 
-  /**
-   * creates a DOM tag element and injects it into the component (this.DOM.scope)
-   * @param  {Object}  tagData [text value & properties for the created tag]
-   * @return {Object} [DOM element]
-   */
-  createTagElem: function createTagElem(tagData) {
-    var tagElm,
-        v = this.escapeHTML(tagData.value),
-        template = this.settings.templates.tag.call(this, v, tagData);
-    if (this.settings.readonly) tagData.readonly = true;
-    template = this.minify(template);
-    tagElm = this.parseHTML(template);
-    return tagElm;
-  },
+        return s2.join('');
+      }).join('');
+      this.DOM.input.innerHTML = s;
+      this.DOM.input.appendChild(document.createTextNode(''));
+      this.DOM.input.normalize();
+      this.getTagElms().forEach((elm, idx) => this.tagData(elm, tagsDataSet[idx]));
+      this.update({
+        withoutChangeEvent: true
+      });
+      return s;
+    },
 
-  /**
-   * Removes a tag
-   * @param  {Object|String}  tagElm          [DOM element or a String value. if undefined or null, remove last added tag]
-   * @param  {Boolean}        silent          [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify]
-   * @param  {Number}         tranDuration    [Transition duration in MS]
-   */
-  removeTag: function removeTag(tagElm, silent, tranDuration) {
-    tagElm = tagElm || this.getLastTag();
-    tranDuration = tranDuration || this.CSSVars.tagHideTransition;
-    if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm);
-    if (!(tagElm instanceof HTMLElement)) return;
-    var tagData,
-        that = this,
-        tagIdx = this.getNodeIndex(tagElm); // this.getTagIndexByValue(tagElm.textContent)
-
-    if (this.settings.mode == 'select') {
-      tranDuration = 0;
-      this.input.set.call(this);
-    }
+    /**
+     * For mixed-mode: replaces a text starting with a prefix with a wrapper element (tag or something)
+     * First there *has* to be a "this.state.tag" which is a string that was just typed and is staring with a prefix
+     */
+    replaceTextWithNode(newWrapperNode, strToReplace) {
+      if (!this.state.tag && !strToReplace) return;
+      strToReplace = strToReplace || this.state.tag.prefix + this.state.tag.value;
+      var idx,
+          nodeToReplace,
+          selection = window.getSelection(),
+          nodeAtCaret = selection.anchorNode,
+          firstSplitOffset = this.state.tag.delimiters ? this.state.tag.delimiters.length : 0; // STEP 1: ex. replace #ba with the tag "bart" where "|" is where the caret is:
+      // CURRENT STATE: "foo #ba #ba| #ba"
+      // split the text node at the index of the caret
+
+      nodeAtCaret.splitText(selection.anchorOffset - firstSplitOffset); // node 0: "foo #ba #ba|"
+      // node 1: " #ba"
+      // get index of LAST occurence of "#ba"
+
+      idx = nodeAtCaret.nodeValue.lastIndexOf(strToReplace);
+      if (idx == -1) return true;
+      nodeToReplace = nodeAtCaret.splitText(idx); // node 0: "foo #ba "
+      // node 1: "#ba"    <- nodeToReplace
+
+      newWrapperNode && nodeAtCaret.parentNode.replaceChild(newWrapperNode, nodeToReplace); // must NOT normalize contenteditable or it will cause unwanted issues:
+      // https://monosnap.com/file/ZDVmRvq5upYkidiFedvrwzSswegWk7
+      // nodeAtCaret.parentNode.normalize()
+
+      return true;
+    },
+
+    /**
+     * For selecting a single option (not used for multiple tags, but for "mode:select" only)
+     * @param {Object} tagElm   Tag DOM node
+     * @param {Object} tagData  Tag data
+     */
+    selectTag(tagElm, tagData) {
+      var _s = this.settings;
+      if (_s.enforceWhitelist && !this.isTagWhitelisted(tagData.value)) return;
+      this.input.set.call(this, tagData[_s.tagTextProp] || tagData.value, true); // place the caret at the end of the input, only if a dropdown option was selected (and not by manually typing another value and clicking "TAB")
+
+      if (this.state.actions.selectOption) setTimeout(this.setRangeAtStartEnd.bind(this));
+      var lastTagElm = this.getLastTag();
+      if (lastTagElm) this.replaceTag(lastTagElm, tagData);else this.appendTag(tagElm);
+      if (_s.enforceWhitelist) this.setContentEditable(false);
+      this.value[0] = tagData;
+      this.update();
+      this.trigger('add', {
+        tag: tagElm,
+        data: tagData
+      });
+      return [tagElm];
+    },
+
+    /**
+     * add an empty "tag" element in an editable state
+     */
+    addEmptyTag(initialData) {
+      var tagData = extend({
+        value: ""
+      }, initialData || {}),
+          tagElm = this.createTagElem(tagData);
+      this.tagData(tagElm, tagData); // add the tag to the component's DOM
+
+      this.appendTag(tagElm);
+      this.editTag(tagElm, {
+        skipValidation: true
+      });
+    },
+
+    /**
+     * add a "tag" element to the "tags" component
+     * @param {String/Array} tagsItems   [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]
+     * @param {Boolean}      clearInput  [flag if the input's value should be cleared after adding tags]
+     * @param {Boolean}      skipInvalid [do not add, mark & remove invalid tags]
+     * @return {Array} Array of DOM elements (tags)
+     */
+    addTags(tagsItems, clearInput, skipInvalid) {
+      var tagElems = [],
+          _s = this.settings,
+          frag = document.createDocumentFragment();
+      skipInvalid = skipInvalid || _s.skipInvalid;
+
+      if (!tagsItems || tagsItems.length == 0) {
+        if (_s.mode == 'select') this.removeAllTags();
+        return tagElems;
+      } // converts Array/String/Object to an Array of Objects
 
-    if (tagElm.classList.contains('tagify--notAllowed')) silent = true;
 
-    function removeNode() {
-      if (!tagElm.parentNode) return;
-      tagElm.parentNode.removeChild(tagElm);
+      tagsItems = this.normalizeTags(tagsItems);
 
-      if (!silent) {
-        tagData = that.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
+      if (_s.mode == 'mix') {
+        return this.addMixTags(tagsItems);
+      }
 
-        that.update(); // update the original input with the current value
+      if (_s.mode == 'select') clearInput = false;
+      this.DOM.input.removeAttribute('style');
+      tagsItems.forEach(tagData => {
+        var tagElm,
+            tagElmParams = {},
+            originalData = Object.assign({}, tagData, {
+          value: tagData.value + ""
+        }); // shallow-clone tagData so later modifications will not apply to the source
 
-        that.trigger('remove', {
-          tag: tagElm,
-          index: tagIdx,
-          data: tagData
-        });
-        that.dropdown.refilter.call(that);
-        that.dropdown.position.call(that);
-      } else if (that.settings.keepInvalidTags) that.trigger('remove', {
-        tag: tagElm,
-        index: tagIdx
-      });
-    }
+        tagData = Object.assign({}, originalData);
 
-    function animation() {
-      tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px';
-      document.body.clientTop; // force repaint for the width to take affect before the "hide" class below
+        _s.transformTag.call(this, tagData);
 
-      tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover)
+        tagData.__isValid = this.hasMaxTags() || this.validateTag(tagData);
 
-      setTimeout(removeNode, tranDuration);
-    }
+        if (tagData.__isValid !== true) {
+          if (skipInvalid) return; // originalData is kept because it might be that this tag is invalid because it is a duplicate of another,
+          // and if that other tags is edited/deleted, this one should be re-validated and if is no more a duplicate - restored
 
-    if (tranDuration && tranDuration > 10) animation();else removeNode();
-  },
-  removeAllTags: function removeAllTags() {
-    this.value = [];
-    this.update();
-    Array.prototype.slice.call(this.getTagElms()).forEach(function (elm) {
-      return elm.parentNode.removeChild(elm);
-    });
-    this.dropdown.position.call(this);
-    if (this.settings.mode == 'select') this.input.set.call(this);
-  },
-  preUpdate: function preUpdate() {
-    this.DOM.scope.classList.toggle('tagify--hasMaxTags', this.value.length >= this.settings.maxTags);
-    this.DOM.scope.classList.toggle('tagify--noTags', !this.value.length);
-  },
+          extend(tagElmParams, this.getInvalidTagAttrs(tagData, tagData.__isValid), {
+            __preInvalidData: originalData
+          });
+          if (tagData.__isValid == this.TEXTS.duplicate) // mark, for a brief moment, the tag (this this one) which THIS CURRENT tag is a duplcate of
+            this.flashTag(this.getTagElmByValue(tagData.value));
+        } /////////////////////////////////////////////////////
 
-  /**
-   * update the origianl (hidden) input field's value
-   * see - https://stackoverflow.com/q/50957841/104380
-   */
-  update: function update() {
-    this.preUpdate();
-    this.DOM.originalInput.value = this.settings.mode == 'mix' ? this.getMixedTagsAsString() : this.value.length ? JSON.stringify(this.value) : "";
-  },
-  getMixedTagsAsString: function getMixedTagsAsString() {
-    var _this11 = this;
-
-    var result = "",
-        i = 0,
-        _interpolator = this.settings.mixTagsInterpolator;
-    this.DOM.input.childNodes.forEach(function (node) {
-      if (node.nodeType == 1 && node.classList.contains("tagify__tag")) result += _interpolator[0] + JSON.stringify(_this11.value[i++]) + _interpolator[1];else result += node.textContent;
-    });
-    return result;
-  },
 
-  /**
-   * Meassures an element's height, which might yet have been added DOM
-   * https://stackoverflow.com/q/5944038/104380
-   * @param {DOM} node
-   */
-  getNodeHeight: function getNodeHeight(node) {
-    var height,
-        clone = node.cloneNode(true);
-    clone.style.cssText = "position:fixed; top:-9999px; opacity:0";
-    document.body.appendChild(clone);
-    height = clone.clientHeight;
-    clone.parentNode.removeChild(clone);
-    return height;
-  },
+        if ('readonly' in tagData) {
+          if (tagData.readonly) tagElmParams["aria-readonly"] = true; // if "readonly" is "false", remove it from the tagData so it won't be added as an attribute in the template
+          else delete tagData.readonly;
+        } // Create tag HTML element
 
-  /**
-   * Dropdown controller
-   * @type {Object}
-   */
-  dropdown: {
-    init: function init() {
-      this.DOM.dropdown = this.dropdown.build.call(this);
-      this.DOM.dropdown.content = this.DOM.dropdown.querySelector('.tagify__dropdown__wrapper');
-    },
-    build: function build() {
-      var _this$settings$dropdo = this.settings.dropdown,
-          position = _this$settings$dropdo.position,
-          classname = _this$settings$dropdo.classname,
-          _className = "".concat(position == 'manual' ? "" : "tagify__dropdown tagify__dropdown--".concat(position), " ").concat(classname).trim(),
-          elm = this.parseHTML("<div class=\"".concat(_className, "\" role=\"listbox\" aria-labelledby=\"dropdown\">\n                        <div class=\"tagify__dropdown__wrapper\"></div>\n                    </div>"));
 
-      return elm;
-    },
-    show: function show(value) {
-      var _this12 = this;
+        tagElm = this.createTagElem(tagData, tagElmParams);
+        tagElems.push(tagElm); // mode-select overrides
 
-      var listHTML,
-          _s = this.settings,
-          firstListItem,
-          firstListItemValue,
-          ddHeight,
-          isManual = _s.dropdown.position == 'manual';
-      if (!_s.whitelist || !_s.whitelist.length || _s.dropdown.enable === false) return; // if no value was supplied, show all the "whitelist" items in the dropdown
-      // @type [Array] listItems
-      // TODO: add a Setting to control items' sort order for "listItems"
+        if (_s.mode == 'select') {
+          return this.selectTag(tagElm, tagData);
+        } // add the tag to the component's DOM
+        // this.appendTag(tagElm)
 
-      this.suggestedListItems = this.dropdown.filterListItems.call(this, value); // hide suggestions list if no suggestions were matched
 
-      if (this.suggestedListItems.length) {
-        firstListItem = this.suggestedListItems[0];
-        firstListItemValue = firstListItem.value || firstListItem;
+        frag.appendChild(tagElm);
 
-        if (_s.autoComplete) {
-          // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of "fuzzysearch" setting)
-          if (firstListItemValue.indexOf(value) == 0) this.input.autocomplete.suggest.call(this, firstListItem);
+        if (tagData.__isValid && tagData.__isValid === true) {
+          // update state
+          this.value.push(tagData);
+          this.trigger('add', {
+            tag: tagElm,
+            index: this.value.length - 1,
+            data: tagData
+          });
+        } else {
+          this.trigger("invalid", {
+            data: tagData,
+            index: this.value.length,
+            tag: tagElm,
+            message: tagData.__isValid
+          });
+          if (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false")
+            setTimeout(() => this.removeTags(tagElm, true), 1000);
         }
-      } else {
-        this.input.autocomplete.suggest.call(this);
-        this.dropdown.hide.call(this);
-        return;
-      }
 
-      listHTML = this.dropdown.createListHTML.call(this, this.suggestedListItems);
-      this.DOM.dropdown.content.innerHTML = this.minify(listHTML); // if "enforceWhitelist" is "true", highlight the first suggested item
-
-      if (_s.enforceWhitelist && !isManual || _s.dropdown.highlightFirst) this.dropdown.highlightOption.call(this, this.DOM.dropdown.content.children[0]);
-      this.DOM.scope.setAttribute("aria-expanded", true);
-      this.trigger("dropdown:show", this.DOM.dropdown); // set the dropdown visible state to be the same as the searched value.
-      // MUST be set *before* position() is called
+        this.dropdown.position(); // reposition the dropdown because the just-added tag might cause a new-line
+      });
+      this.appendTag(frag);
+      this.update();
 
-      this.state.dropdown.visible = value || true;
-      this.dropdown.position.call(this); // if the dropdown has yet to be appended to the document,
-      // append the dropdown to the body element & handle events
+      if (tagsItems.length && clearInput) {
+        this.input.set.call(this);
+      }
 
-      if (!document.body.contains(this.DOM.dropdown)) {
-        if (!isManual) {
-          this.events.binding.call(this, false); // unbind the main events
-          // let the element render in the DOM first to accurately measure it
-          // this.DOM.dropdown.style.cssText = "left:-9999px; top:-9999px;";
-
-          ddHeight = this.getNodeHeight(this.DOM.dropdown);
-          this.DOM.dropdown.classList.add('tagify__dropdown--initial');
-          this.dropdown.position.call(this, ddHeight);
-          document.body.appendChild(this.DOM.dropdown);
-          setTimeout(function () {
-            return _this12.DOM.dropdown.classList.remove('tagify__dropdown--initial');
-          });
-        } // timeout is needed for when pressing arrow down to show the dropdown,
-        // so the key event won't get registered in the dropdown events listeners
+      this.dropdown.refilter();
+      return tagElems;
+    },
 
+    /**
+     * Adds a mix-content tag
+     * @param {String/Array} tagData    A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings
+     */
+    addMixTags(tagsData) {
+      tagsData = this.normalizeTags(tagsData);
 
-        setTimeout(this.dropdown.events.binding.bind(this));
+      if (tagsData[0].prefix || this.state.tag) {
+        return this.prefixedTextToTag(tagsData[0]);
       }
-    },
-    hide: function hide(force) {
-      var _this$DOM = this.DOM,
-          scope = _this$DOM.scope,
-          dropdown = _this$DOM.dropdown,
-          isManual = this.settings.dropdown.position == 'manual' && !force;
-      if (!dropdown || !document.body.contains(dropdown) || isManual) return;
-      window.removeEventListener('resize', this.dropdown.position);
-      this.dropdown.events.binding.call(this, false); // unbind all events
-      // must delay because if the dropdown is open, and the input (scope) is clicked,
-      // the dropdown should be now closed, and the next click should re-open it,
-      // and without this timeout, clicking to close will re-open immediately
 
-      setTimeout(this.events.binding.bind(this), 250); // re-bind main events
+      if (typeof tagsData == 'string') tagsData = [{
+        value: tagsData
+      }];
+      var selection = !!this.state.selection,
+          // must be cast, not to use the reference which is changing
+      frag = document.createDocumentFragment();
+      tagsData.forEach(tagData => {
+        var tagElm = this.createTagElem(tagData);
+        frag.appendChild(tagElm);
+        this.insertAfterTag(tagElm);
+      }); // if "selection" exists, assumes intention of inecting the new tag at the last
+      // saved location of the caret inside "this.DOM.input"
+
+      if (selection) {
+        this.injectAtCaret(frag);
+      } // else, create a range and inject the new tag as the last child of "this.DOM.input"
+      else {
+        this.DOM.input.focus();
+        selection = this.setStateSelection();
+        selection.range.setStart(this.DOM.input, selection.range.endOffset);
+        selection.range.setEnd(this.DOM.input, selection.range.endOffset);
+        this.DOM.input.appendChild(frag);
+        this.updateValueByDOMTags(); // updates internal "this.value"
+
+        this.update(); // updates original input/textarea
+      }
 
-      scope.setAttribute("aria-expanded", false);
-      dropdown.parentNode.removeChild(dropdown);
-      this.state.dropdown.visible = false;
-      this.state.ddItemData = null;
-      this.state.ddItemElm = null;
-      this.trigger("dropdown:hide", dropdown);
+      return frag;
     },
 
     /**
-     * fill data into the suggestions list (mainly used to update the list when removing tags, so they will be re-added to the list. not efficient)
+     * Adds a tag which was activly typed by the user
+     * @param {String/Array} tagItem   [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]
      */
-    refilter: function refilter() {
-      this.suggestedListItems = this.dropdown.filterListItems.call(this, '');
-      var listHTML = this.dropdown.createListHTML.call(this, this.suggestedListItems);
-      this.DOM.dropdown.content.innerHTML = this.minify(listHTML);
-    },
-    position: function position(ddHeight) {
-      var isBelowViewport,
-          rect,
-          top,
-          bottom,
-          left,
-          width,
-          ddElm = this.DOM.dropdown;
-      if (!this.state.dropdown.visible) return;
+    prefixedTextToTag(tagItem) {
+      var _s = this.settings,
+          tagElm,
+          createdFromDelimiters = this.state.tag.delimiters;
 
-      if (this.settings.dropdown.position == 'text') {
-        rect = this.getCaretGlobalPosition();
-        bottom = rect.bottom;
-        top = rect.top;
-        left = rect.left;
-        width = 'auto';
-      } else {
-        rect = this.DOM.scope.getBoundingClientRect();
-        top = rect.top;
-        bottom = rect.bottom - 1;
-        left = rect.left;
-        width = rect.width + "px";
-      }
+      _s.transformTag.call(this, tagItem);
 
-      top = Math.floor(top);
-      bottom = Math.ceil(bottom);
-      isBelowViewport = document.documentElement.clientHeight - bottom < (ddHeight || ddElm.clientHeight); // flip vertically if there is no space for the dropdown below the input
+      tagItem.prefix = tagItem.prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source || _s.pattern)[0]; // TODO: should check if the tag is valid
 
-      ddElm.style.cssText = "left:" + (left + window.pageXOffset) + "px; width:" + width + ";" + (isBelowViewport ? "bottom:" + (document.documentElement.clientHeight - top - window.pageYOffset - 2) + "px;" : "top: " + (bottom + window.pageYOffset) + "px");
-      ddElm.setAttribute('placement', isBelowViewport ? "top" : "bottom");
-    },
-    events: {
-      /**
-       * Events should only be binded when the dropdown is rendered and removed when isn't
-       * @param  {Boolean} bindUnbind [optional. true when wanting to unbind all the events]
-       */
-      binding: function binding() {
-        var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+      tagElm = this.createTagElem(tagItem); // tries to replace a taged textNode with a tagElm, and if not able,
+      // insert the new tag to the END if "addTags" was called from outside
 
-        // references to the ".bind()" methods must be saved so they could be unbinded later
-        var _CB = this.dropdown.events.callbacks,
-            _CBR = this.listeners.dropdown = this.listeners.dropdown || {
-          position: this.dropdown.position.bind(this),
-          onKeyDown: _CB.onKeyDown.bind(this),
-          onMouseOver: _CB.onMouseOver.bind(this),
-          onMouseLeave: _CB.onMouseLeave.bind(this),
-          onClick: _CB.onClick.bind(this)
-        },
-            action = bindUnbind ? 'addEventListener' : 'removeEventListener';
+      if (!this.replaceTextWithNode(tagElm)) {
+        this.DOM.input.appendChild(tagElm);
+      }
 
-        if (this.settings.dropdown.position != 'manual') {
-          window[action]('resize', _CBR.position);
-          window[action]('keydown', _CBR.onKeyDown);
-        } //  window[action]('mousedown', _CBR.onClick);
+      setTimeout(() => tagElm.classList.add(this.settings.classNames.tagNoAnimation), 300);
+      this.value.push(tagItem);
+      this.update();
 
+      if (!createdFromDelimiters) {
+        var elm = this.insertAfterTag(tagElm) || tagElm;
+        this.placeCaretAfterNode(elm);
+      }
 
-        this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver);
-        this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave);
-        this.DOM.dropdown[action]('mousedown', _CBR.onClick); // add back the main "click" event because it is needed for removing/clicking already-existing tags, even if dropdown is shown
+      this.state.tag = null;
+      this.trigger('add', extend({}, {
+        tag: tagElm
+      }, {
+        data: tagItem
+      }));
+      return tagElm;
+    },
 
-        this.DOM[this.listeners.main.click[0]][action]('click', this.listeners.main.click[1]);
-      },
-      callbacks: {
-        onKeyDown: function onKeyDown(e) {
-          // get the "active" element, and if there was none (yet) active, use first child
-          var activeListElm = this.DOM.dropdown.querySelector("[class$='--active']"),
-              selectedElm = activeListElm;
+    /**
+     * appened (validated) tag to the component's DOM scope
+     */
+    appendTag(tagElm) {
+      var DOM = this.DOM,
+          insertBeforeNode = DOM.scope.lastElementChild;
+      if (insertBeforeNode === DOM.input) DOM.scope.insertBefore(tagElm, insertBeforeNode);else DOM.scope.appendChild(tagElm);
+    },
 
-          switch (e.key) {
-            case 'ArrowDown':
-            case 'ArrowUp':
-            case 'Down': // >IE11
+    /**
+     * creates a DOM tag element and injects it into the component (this.DOM.scope)
+     * @param  {Object}  tagData [text value & properties for the created tag]
+     * @param  {Object}  extraData [properties which are for the HTML template only]
+     * @return {Object} [DOM element]
+     */
+    createTagElem(tagData, extraData) {
+      tagData.__tagId = getUID();
+      var tagElm,
+          templateData = extend({}, tagData, _objectSpread2({
+        value: escapeHTML(tagData.value + "")
+      }, extraData)); // if( this.settings.readonly )
+      //     tagData.readonly = true
 
-            case 'Up':
-              {
-                // >IE11
-                e.preventDefault();
-                var dropdownItems;
-                if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop
+      tagElm = this.parseTemplate('tag', [templateData]); // crucial for proper caret placement when deleting content. if textNodes are allowed as children of
+      // a tag element, a browser bug casues the caret to misplaced inside the tag element (especcially affects "readonly" tags)
 
-                if (!selectedElm) {
-                  dropdownItems = this.DOM.dropdown.content.children;
-                  selectedElm = dropdownItems[e.key == 'ArrowUp' || e.key == 'Up' ? dropdownItems.length - 1 : 0];
-                }
+      removeTextChildNodes(tagElm); // while( tagElm.lastChild.nodeType == 3 )
+      //     tagElm.lastChild.parentNode.removeChild(tagElm.lastChild)
 
-                this.dropdown.highlightOption.call(this, selectedElm, true);
-                break;
-              }
+      this.tagData(tagElm, tagData);
+      return tagElm;
+    },
 
-            case 'Escape':
-            case 'Esc':
-              // IE11
-              this.dropdown.hide.call(this);
-              break;
+    /**
+     * re-check all invalid tags.
+     * called after a tag was edited or removed
+     */
+    reCheckInvalidTags() {
+      var _s = this.settings;
+      this.getTagElms(_s.classNames.tagNotAllowed).forEach((tagElm, i) => {
+        var tagData = this.tagData(tagElm),
+            hasMaxTags = this.hasMaxTags(),
+            tagValidation = this.validateTag(tagData); // if the tag has become valid
+
+        if (tagValidation === true && !hasMaxTags) {
+          tagData = tagData.__preInvalidData ? tagData.__preInvalidData : {
+            value: tagData.value
+          };
+          return this.replaceTag(tagElm, tagData);
+        } // if the tag is still invaild, set its title as such (reson of invalid might have changed)
+
+
+        tagElm.title = hasMaxTags || tagValidation;
+      });
+    },
 
-            case 'ArrowRight':
-              if (this.state.actions.ArrowLeft) return;
+    /**
+     * Removes a tag
+     * @param  {Array|Node|String}  tagElms         [DOM element(s) or a String value. if undefined or null, remove last added tag]
+     * @param  {Boolean}            silent          [A flag, which when turned on, does not remove any value and does not update the original input value but simply removes the tag from tagify]
+     * @param  {Number}             tranDuration    [Transition duration in MS]
+     * TODO: Allow multiple tags to be removed at-once
+     */
+    removeTags(tagElms, silent, tranDuration) {
+      var tagsToRemove;
+      tagElms = tagElms && tagElms instanceof HTMLElement ? [tagElms] : tagElms instanceof Array ? tagElms : tagElms ? [tagElms] : [this.getLastTag()]; // normalize tagElms array values:
+      // 1. removing invalid items
+      // 2, if an item is String try to get the matching Tag HTML node
+      // 3. get the tag data
+      // 4. return a collection of Objects
+
+      tagsToRemove = tagElms.reduce((elms, tagElm) => {
+        if (tagElm && typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm);
+        var tagData = this.tagData(tagElm);
+        if (tagElm && tagData && !tagData.readonly) // make sure it's a tag and not some other node
+          // because the DOM node might be removed by async animation, the state will be updated while
+          // the node might still be in the DOM, so the "update" method should know which nodes to ignore
+          elms.push({
+            node: tagElm,
+            idx: this.getTagIdx(tagData),
+            // this.getNodeIndex(tagElm); // this.getTagIndexByValue(tagElm.textContent)
+            data: this.tagData(tagElm, {
+              '__removed': true
+            })
+          });
+        return elms;
+      }, []);
+      tranDuration = typeof tranDuration == "number" ? tranDuration : this.CSSVars.tagHideTransition;
 
-            case 'Tab':
-              {
-                e.preventDefault(); // in mix-mode, treat arrowRight like Enter key, so a tag will be created
+      if (this.settings.mode == 'select') {
+        tranDuration = 0;
+        this.input.set.call(this);
+      } // if only a single tag is to be removed
 
-                if (this.settings.mode != 'mix' && !this.settings.autoComplete.rightKey) {
-                  try {
-                    var value = selectedElm ? selectedElm.textContent : this.suggestedListItems[0].value;
-                    this.input.autocomplete.set.call(this, value);
-                  } catch (err) {}
 
-                  return false;
-                }
-              }
+      if (tagsToRemove.length == 1) {
+        if (tagsToRemove[0].node.classList.contains(this.settings.classNames.tagNotAllowed)) silent = true;
+      }
 
-            case 'Enter':
-              {
-                e.preventDefault();
-                this.dropdown.selectOption.call(this, activeListElm);
-                break;
-              }
+      if (!tagsToRemove.length) return;
+      return this.settings.hooks.beforeRemoveTag(tagsToRemove, {
+        tagify: this
+      }).then(() => {
+        function removeNode(tag) {
+          if (!tag.node.parentNode) return;
+          tag.node.parentNode.removeChild(tag.node);
+
+          if (!silent) {
+            // this.removeValueById(tagData.__uid)
+            this.trigger('remove', {
+              tag: tag.node,
+              index: tag.idx,
+              data: tag.data
+            });
+            this.dropdown.refilter();
+            this.dropdown.position();
+            this.DOM.input.normalize(); // best-practice when in mix-mode (safe to do always anyways)
+            // check if any of the current tags which might have been marked as "duplicate" should be un-marked
+
+            if (this.settings.keepInvalidTags) this.reCheckInvalidTags();
+          } else if (this.settings.keepInvalidTags) this.trigger('remove', {
+            tag: tag.node,
+            index: tag.idx
+          });
+        }
 
-            case 'Backspace':
-              {
-                if (this.settings.mode == 'mix' || this.state.editing.scope) return;
+        function animation(tag) {
+          tag.node.style.width = parseFloat(window.getComputedStyle(tag.node).width) + 'px';
+          document.body.clientTop; // force repaint for the width to take affect before the "hide" class below
 
-                var _value = this.input.value.trim();
+          tag.node.classList.add(this.settings.classNames.tagHide); // manual timeout (hack, since transitionend cannot be used because of hover)
 
-                if (_value == "" || _value.charCodeAt(0) == 8203) {
-                  if (this.settings.backspace === true) this.removeTag();else if (this.settings.backspace == 'edit') setTimeout(this.editTag.bind(this), 0);
-                }
-              }
-          }
-        },
-        onMouseOver: function onMouseOver(e) {
-          var ddItem = e.target.closest('.tagify__dropdown__item'); // event delegation check
+          setTimeout(removeNode.bind(this), tranDuration, tag);
+        }
 
-          ddItem && this.dropdown.highlightOption.call(this, ddItem);
-        },
-        onMouseLeave: function onMouseLeave(e) {
-          // de-highlight any previously highlighted option
-          this.dropdown.highlightOption.call(this);
-        },
-        onClick: function onClick(e) {
-          if (e.button != 0 || e.target == this.DOM.dropdown) return; // allow only mouse left-clicks
+        if (tranDuration && tranDuration > 10 && tagsToRemove.length == 1) animation.call(this, tagsToRemove[0]);else tagsToRemove.forEach(removeNode.bind(this)); // update state regardless of animation
 
-          var listItemElm = e.target.closest(".tagify__dropdown__item");
-          this.dropdown.selectOption.call(this, listItemElm);
+        if (!silent) {
+          this.removeTagsFromValue(tagsToRemove.map(tag => tag.node));
+          this.update(); // update the original input with the current value
+
+          if (this.settings.mode == 'select') this.setContentEditable(true);
         }
-      }
+      }).catch(reason => {});
+    },
+
+    removeTagsFromDOM() {
+      [].slice.call(this.getTagElms()).forEach(elm => elm.parentNode.removeChild(elm));
     },
 
     /**
-     * mark the currently active suggestion option
-     * @param {Object}  elm            option DOM node
-     * @param {Boolean} adjustScroll   when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element
+     * @param {Array/Node} tags to be removed from the this.value array
      */
-    highlightOption: function highlightOption(elm, adjustScroll) {
-      var className = "tagify__dropdown__item--active",
-          itemData; // focus casues a bug in Firefox with the placeholder been shown on the input element
-      // if( this.settings.dropdown.position != 'manual' )
-      //     elm.focus();
+    removeTagsFromValue(tags) {
+      tags = Array.isArray(tags) ? tags : [tags];
+      tags.forEach(tag => {
+        var tagData = this.tagData(tag),
+            tagIdx = this.getTagIdx(tagData); //  delete tagData.__removed
 
-      if (this.state.ddItemElm) {
-        this.state.ddItemElm.classList.remove(className);
-        this.state.ddItemElm.removeAttribute("aria-selected");
-      }
+        if (tagIdx > -1) this.value.splice(tagIdx, 1);
+      });
+    },
 
-      if (!elm) {
-        this.state.ddItemData = null;
-        this.state.ddItemElm = null;
-        this.input.autocomplete.suggest.call(this);
-        return;
-      }
+    removeAllTags(opts) {
+      opts = opts || {};
+      this.value = [];
+      if (this.settings.mode == 'mix') this.DOM.input.innerHTML = '';else this.removeTagsFromDOM();
+      this.dropdown.position();
 
-      itemData = this.suggestedListItems[this.getNodeIndex(elm)];
-      this.state.ddItemData = itemData;
-      this.state.ddItemElm = elm; // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
+      if (this.settings.mode == 'select') {
+        this.input.set.call(this);
+        this.setContentEditable(true);
+      } // technically for now only "withoutChangeEvent" exists in the opts.
+      // if more properties will be added later, only pass what's needed to "update"
 
-      elm.classList.add(className);
-      elm.setAttribute("aria-selected", true);
-      if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; // Try to autocomplete the typed value with the currently highlighted dropdown item
 
-      if (this.settings.autoComplete) {
-        this.input.autocomplete.suggest.call(this, itemData);
-        if (this.settings.dropdown.position != 'manual') this.dropdown.position.call(this); // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line
-      }
+      this.update(opts);
     },
 
-    /**
-     * Create a tag from the currently active suggestion option
-     * @param {Object} elm  DOM node to select
-     */
-    selectOption: function selectOption(elm) {
-      var _this13 = this;
+    postUpdate() {
+      var classNames = this.settings.classNames,
+          hasValue = this.settings.mode == 'mix' ? this.settings.mixMode.integrated ? this.DOM.input.textContent : this.DOM.originalInput.value.trim() : this.value.length + this.input.raw.call(this).length;
+      this.toggleClass(classNames.hasMaxTags, this.value.length >= this.settings.maxTags);
+      this.toggleClass(classNames.hasNoTags, !this.value.length);
+      this.toggleClass(classNames.empty, !hasValue);
+    },
 
-      if (!elm) return; // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run
+    setOriginalInputValue(v) {
+      var inputElm = this.DOM.originalInput;
 
-      this.state.actions.selectOption = true;
-      setTimeout(function () {
-        return _this13.state.actions.selectOption = false;
-      }, 50);
-      var hideDropdown = this.settings.dropdown.closeOnSelect,
-          value = this.suggestedListItems[this.getNodeIndex(elm)] || this.input.value;
-      this.trigger("dropdown:select", value);
-      this.addTags([value], true); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing
+      if (!this.settings.mixMode.integrated) {
+        inputElm.value = v;
+        inputElm.tagifyValue = inputElm.value; // must set to "inputElm.value" and not again to "inputValue" because for some reason the browser changes the string afterwards a bit.
 
-      setTimeout(function () {
-        _this13.DOM.input.focus();
+        this.setPersistedData(v, 'value');
+      }
+    },
 
-        _this13.toggleFocusClass(true);
-      });
+    /**
+     * update the origianl (hidden) input field's value
+     * see - https://stackoverflow.com/q/50957841/104380
+     */
+    update(args) {
+      var inputValue = this.getInputValue();
+      this.setOriginalInputValue(inputValue);
+      this.postUpdate();
+      if (!(args || {}).withoutChangeEvent && !this.state.blockChangeEvent) this.triggerChangeEvent();
+    },
 
-      if (hideDropdown) {
-        this.dropdown.hide.call(this); // setTimeout(() => this.events.callbacks.onFocusBlur.call(this, {type:"blur"}), 60)
-      }
+    getInputValue() {
+      var value = this.getCleanValue();
+      return this.settings.mode == 'mix' ? this.getMixedTagsAsString(value) : value.length ? this.settings.originalInputValueFormat ? this.settings.originalInputValueFormat(value) : JSON.stringify(value) : "";
     },
 
     /**
-     * returns an HTML string of the suggestions' list items
-     * @param {string} value string to filter the whitelist by
-     * @return {Array} list of filtered whitelist items according to the settings provided and current value
+     * removes properties from `this.value` which are only used internally
      */
-    filterListItems: function filterListItems(value) {
-      var _this14 = this;
+    getCleanValue(v) {
+      return removeCollectionProp(v || this.value, this.dataProps);
+    },
 
-      var _s = this.settings,
-          list = [],
-          whitelist = _s.whitelist,
-          suggestionsCount = _s.dropdown.maxItems || Infinity,
-          searchKeys = _s.dropdown.searchKeys.concat(["searchBy", "value"]),
-          whitelistItem,
-          valueIsInWhitelist,
-          whitelistItemValueIndex,
-          searchBy,
-          isDuplicate,
-          i = 0;
+    getMixedTagsAsString() {
+      var result = "",
+          that = this,
+          _interpolator = this.settings.mixTagsInterpolator;
 
-      if (!value) {
-        return (_s.duplicates ? whitelist : whitelist.filter(function (item) {
-          return !_this14.isTagDuplicate(item.value || item);
-        }) // don't include tags which have already been added.
-        ).slice(0, suggestionsCount); // respect "maxItems" dropdown setting
-      }
+      function iterateChildren(rootNode) {
+        rootNode.childNodes.forEach(node => {
+          if (node.nodeType == 1) {
+            const tagData = that.tagData(node);
 
-      for (; i < whitelist.length; i++) {
-        whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : {
-          value: whitelist[i]
-        }; //normalize value as an Object
+            if (node.tagName == 'BR') {
+              result += "\r\n";
+            }
 
-        searchBy = searchKeys.reduce(function (values, k) {
-          return values + " " + (whitelistItem[k] || "");
-        }, "").toLowerCase();
-        whitelistItemValueIndex = searchBy.indexOf(value.toLowerCase());
-        valueIsInWhitelist = _s.dropdown.fuzzySearch ? whitelistItemValueIndex >= 0 : whitelistItemValueIndex == 0;
-        isDuplicate = !_s.duplicates && this.isTagDuplicate(whitelistItem.value); // match for the value within each "whitelist" item
+            if (node.tagName == 'DIV' || node.tagName == 'P') {
+              result += "\r\n"; //  if( !node.children.length && node.textContent )
+              //  result += node.textContent;
 
-        if (valueIsInWhitelist && !isDuplicate && suggestionsCount--) list.push(whitelistItem);
-        if (suggestionsCount == 0) break;
+              iterateChildren(node);
+            } else if (isNodeTag.call(that, node) && tagData) {
+              if (tagData.__removed) return;else result += _interpolator[0] + JSON.stringify(omit(tagData, that.dataProps)) + _interpolator[1];
+            }
+          } else result += node.textContent;
+        });
       }
 
-      return list;
-    },
-
-    /**
-     * Creates the dropdown items' HTML
-     * @param  {Array} list  [Array of Objects]
-     * @return {String}
-     */
-    createListHTML: function createListHTML(optionsArr) {
-      var template = this.settings.templates.dropdownItem.bind(this);
-      return this.minify(optionsArr.map(template).join(""));
+      iterateChildren(this.DOM.input);
+      return result;
     }
-  }
-};
-return Tagify;
-}));
+
+  }; // legacy support for changed methods names
+
+  Tagify.prototype.removeTag = Tagify.prototype.removeTags;
+
+  return Tagify;
+
+})));
index 24c19a9b167fdd272e0cd286bffeebfe508e0b41..680ddaedc4f89da80aabbcdc1cd9d75d56f6b2da 100644 (file)
@@ -1,7 +1,8 @@
 /**
- * Tagify (v 3.2.6)- tags input component
+ * Tagify (v 4.9.8) - tags input component
  * By Yair Even-Or
  * Don't sell this code. (c)
  * https://github.com/yairEO/tagify
  */
-!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function u(t){return function(t){if(Array.isArray(t)){for(var e=0,i=new Array(t.length);e<t.length;e++)i[e]=t[e];return i}}(t)||function(t){if(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t))return Array.from(t)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}function s(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),i.push.apply(i,s)}return i}function g(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?s(i,!0).forEach(function(t){n(e,t,i[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):s(i).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))})}return e}function n(t,e,i){return e in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;this.applySettings(t,e||{}),this.state={editing:{},actions:{},dropdown:{}},this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus()}return t.prototype={isIE:window.document.documentMode,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,editTags:2,transformTag:function(){},autoComplete:{enabled:!0,rightKey:!1},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:[],fuzzySearch:!0,highlightFirst:!1,closeOnSelect:!0,position:"all"}},templates:{wrapper:function(t,e){return'<tags class="tagify '.concat(e.mode?"tagify--"+e.mode:""," ").concat(t.className,'"\n                        ').concat(e.readonly?'readonly aria-readonly="true"':'aria-haspopup="listbox" aria-expanded="false"','\n                        role="tagslist"\n                        tabIndex="-1">\n                <span contenteditable data-placeholder="').concat(e.placeholder||"&#8203;",'" aria-placeholder="').concat(e.placeholder||"",'"\n                    class="tagify__input"\n                    role="textbox"\n                    aria-controls="dropdown"\n                    aria-autocomplete="both"\n                    aria-multiline="').concat("mix"==e.mode,'"></span>\n            </tags>')},tag:function(t,e){return"<tag title='".concat(e.title||t,"'\n                        contenteditable='false'\n                        spellcheck='false'\n                        tabIndex=\"-1\"\n                        class='tagify__tag ").concat(e.class?e.class:"","'\n                        ").concat(this.getAttributes(e),">\n                <x title='' class='tagify__tag__removeBtn' role='button' aria-label='remove tag'></x>\n                <div>\n                    <span class='tagify__tag-text'>").concat(t,"</span>\n                </div>\n            </tag>")},dropdownItem:function(t){var e=this.settings.dropdown.mapValueTo,i=((e?"function"==typeof e?e(t):t[e]:t.value)||t.value||t).replace(/`|'/g,"&#39;");return"<div ".concat(this.getAttributes(t),"\n                        class='tagify__dropdown__item ").concat(t.class?t.class:"",'\'\n                        tabindex="0"\n                        role="option"\n                        aria-labelledby="dropdown-label">').concat(i,"</div>")}},customEventsList:["add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select"],applySettings:function(i,t){var s=this;if(this.DEFAULTS.templates=this.templates,this.settings=this.extend({},this.DEFAULTS,t),this.settings.readonly=i.hasAttribute("readonly"),this.settings.placeholder=i.getAttribute("placeholder")||this.settings.placeholder||"",this.isIE&&(this.settings.autoComplete=!1),["whitelist","blacklist"].forEach(function(t){var e=i.getAttribute("data-"+t);e&&(e=e.split(s.settings.delimiters))instanceof Array&&(s.settings[t]=e)}),"autoComplete"in t&&!this.isObject(t.autoComplete)&&(this.settings.autoComplete=this.DEFAULTS.autoComplete,this.settings.autoComplete.enabled=t.autoComplete),i.pattern)try{this.settings.pattern=new RegExp(i.pattern)}catch(t){}if(this.settings.delimiters)try{this.settings.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}"select"==this.settings.mode&&(this.settings.dropdown.enabled=0),"mix"==this.settings.mode&&(this.settings.autoComplete.rightKey=!0)},getAttributes:function(t){if("[object Object]"!=Object.prototype.toString.call(t))return"";var e,i,s=Object.keys(t),n="";for(i=s.length;i--;)"class"!=(e=s[i])&&t.hasOwnProperty(e)&&t[e]&&(n+=" "+e+(t[e]?'="'.concat(t[e],'"'):""));return n},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},escapeHTML:function(t){var e=document.createTextNode(t),i=document.createElement("p");return i.appendChild(e),i.innerHTML},getCaretGlobalPosition:function(){var t=document.getSelection();if(t.rangeCount){var e,i,s=t.getRangeAt(0),n=s.startContainer,a=s.startOffset;if(0<a)return(i=document.createRange()).setStart(n,a-1),i.setEnd(n,a),{left:(e=i.getBoundingClientRect()).right,top:e.top,bottom:e.bottom}}return{left:-9999,top:-9999}},getCSSVars:function(){var t,e,i,s=getComputedStyle(this.DOM.scope,null);this.CSSVars={tagHideTransition:(t=function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter(function(t){return t}).pop().trim();return{value:+t.split(e).filter(function(t){return t})[0].trim(),unit:e}}((i="tag-hide-transition",s.getPropertyValue("--"+i))),e=t.value,"s"==t.unit?1e3*e:e)}},build:function(t){var e=this.DOM,i=this.settings.templates.wrapper(t,this.settings);e.originalInput=t,e.scope=this.parseHTML(i),e.input=e.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(e.scope,t),0<=this.settings.dropdown.enabled&&this.dropdown.init.call(this)},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide.call(this,!0)},loadOriginalValues:function(t){if(t=t||this.DOM.originalInput.value)if(this.removeAllTags(),"mix"==this.settings.mode)this.parseMixTags(t.trim());else{try{"string"!=typeof JSON.parse(t)&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach(function(t){return t&&t.classList.add("tagify--noAnim")})}},isObject:function(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e},extend:function(t,e,i){var s=this;function n(t,e){for(var i in e)e.hasOwnProperty(i)&&(s.isObject(e[i])?s.isObject(t[i])?n(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),n(t,e),i&&n(t,i),t},cloneEvent:function(t){var e={};for(var i in t)e[i]=t[i];return e},EventDispatcher:function(s){var n=document.createTextNode("");function i(e,t,i){i&&t.split(/\s+/g).forEach(function(t){return n[e+"EventListener"].call(n,t,i)})}this.off=function(t,e){return i("remove",t,e),this},this.on=function(t,e){return e&&"function"==typeof e&&i("add",t,e),this},this.trigger=function(t,e){var i;if(t)if(s.settings.isJQueryPlugin)"remove"==t&&(t="removeTag"),jQuery(s.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:this.extend({},e,{tagify:this})})}catch(t){console.warn(t)}n.dispatchEvent(i)}}},loading:function(t){return this.DOM.scope.classList[t?"add":"remove"]("tagify--loading"),this},toggleFocusClass:function(t){this.DOM.scope.classList.toggle("tagify--focus",!!t)},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(t){var e,i=!(0<arguments.length&&void 0!==t)||t,s=this.events.callbacks,n=i?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!i)for(var a in(this.state.mainEvents=i)&&!this.listeners.main&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",s[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",s.onFocusBlur.bind(this)],blur:["input",s.onFocusBlur.bind(this)],keydown:["input",s.onKeydown.bind(this)],click:["scope",s.onClickScope.bind(this)],dblclick:["scope",s.onDoubleClickScope.bind(this)]}){if("blur"==a&&!i)return;this.DOM[e[a][0]][n](a,e[a][1])}},callbacks:{onFocusBlur:function(t){var e=t.target?t.target.textContent.trim():"",i=this.settings,s=t.type;if(!(t.relatedTarget&&t.relatedTarget.classList.contains("tagify__tag")&&this.DOM.scope.contains(t.relatedTarget))){if("blur"==s&&t.relatedTarget===this.DOM.scope)return this.dropdown.hide.call(this),void this.DOM.input.focus();if(!this.state.actions.selectOption||!i.dropdown.enabled&&i.dropdown.closeOnSelect)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),this.setRangeAtStartEnd(!1),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",{relatedTarget:t.relatedTarget}),void(0===i.dropdown.enabled&&"select"!=i.mode&&this.dropdown.show.call(this));"blur"==s&&(this.trigger("blur",{relatedTarget:t.relatedTarget}),this.loading(!1),e&&!this.state.actions.selectOption&&i.addTagOnBlur&&this.addTags(e,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this)}else"blur"==t.type&&this.dropdown.hide.call(this)}},onKeydown:function(t){var e,i=this,s=t.target.textContent.trim();if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==this.settings.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":var n=document.getSelection();!!navigator.userAgent.match(/firefox/i)&&n&&0==n.anchorOffset&&this.removeTag(n.anchorNode.previousSibling);var a=[];e=this.DOM.input.children,setTimeout(function(){[].forEach.call(e,function(t){return a.push(t.getAttribute("value"))}),i.value=i.value.filter(function(t){return-1!=a.indexOf(t.value)})})}return!0}switch(t.key){case"Backspace":""!=s&&8203!=s.charCodeAt(0)||(!0===this.settings.backspace?this.removeTag():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show.call(this);break;case"ArrowRight":var o=this.state.inputSuggestion||this.state.ddItemData;if(o&&this.settings.autoComplete.rightKey)return void this.addTags([o],!0);break;case"Tab":if(!s)return!0;case"Enter":t.preventDefault(),setTimeout(function(){i.state.actions.selectOption||i.addTags(s,!0)})}},onInput:function(t){var e="mix"==this.settings.mode?this.DOM.input.textContent:this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);e?this.input.value!=e&&(s.isValid=this.validateTag(e),this.trigger("input",s),this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onMixTagsInput:function(){var t,e,i,s,n,a=this,o=this.settings;if(this.hasMaxTags())return!0;window.getSelection&&0<(t=window.getSelection()).rangeCount&&((e=t.getRangeAt(0).cloneRange()).collapse(!0),e.setStart(window.getSelection().focusNode,0),(s=(i=e.toString().split(o.mixTagsAllowedAfter))[i.length-1].match(o.pattern))&&(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:s[0],value:s.input.split(s[0])[1]},n=this.state.tag.value.length>=o.dropdown.enabled)),this.update(),setTimeout(function(){a.trigger("input",a.extend({},a.state.tag,{textContent:a.DOM.input.textContent})),a.state.tag&&a.dropdown[n?"show":"hide"].call(a,a.state.tag.value)},10)},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onClickScope:function(t){var e,i=t.target.closest(".tagify__tag"),s=this.settings,n=new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains("tagify__tag__removeBtn"))return i?(e=this.getNodeIndex(i),this.trigger("click",{tag:i,index:e,data:this.value[e],originalEvent:this.cloneEvent(t)}),void(1==this.settings.editTags&&this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&500<n?this.state.dropdown.visible?this.dropdown.hide.call(this):0===s.dropdown.enabled&&"mix"!=s.mode&&this.dropdown.show.call(this):"select"==s.mode&&(this.state.dropdown.visible||this.dropdown.show.call(this)));this.removeTag(t.target.parentNode)}else this.DOM.input.focus()},onEditTagInput:function(t,e){var i=t.closest("tag"),s=this.getNodeIndex(i),n=this.input.normalize.call(this,t),a=n.toLowerCase()==t.originalValue.toLowerCase()||this.validateTag(n);i.classList.toggle("tagify--invalid",!0!==a),i.isValid=a,n.length>=this.settings.dropdown.enabled&&(this.state.editing.value=n,this.dropdown.show.call(this,n)),this.trigger("edit:input",{tag:i,index:s,data:this.extend({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagBlur:function(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e=t.closest(".tagify__tag"),i=this.getNodeIndex(e),s=this.input.normalize.call(this,t),n=s||t.originalValue,a=n!=t.originalValue,o=e.isValid,r=g({},this.value[i],{value:n});s?a?(this.settings.transformTag.call(this,r),void 0!==(o=this.validateTag(r.value))&&!0!==o||this.onEditTagDone(e,r)):this.onEditTagDone(e):this.removeTag(e)}},onEditTagkeydown:function(t){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":t.target.textContent=t.target.originalValue;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope:function(t){var e,i,s=t.target.closest("tag"),n=this.settings;s&&(e=s.classList.contains("tagify__tag--editable"),i=s.hasAttribute("readonly"),"select"==n.mode||n.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0))}}},editTag:function(t){var e=this,i=0<arguments.length&&void 0!==t?t:this.getLastTag(),s=i.querySelector(".tagify__tag-text"),n=this.getNodeIndex(i),a=this.value[n],o=this.events.callbacks,r=this;if(s){if(!("editable"in a)||a.editable)return i.classList.add("tagify__tag--editable"),s.originalValue=s.textContent,s.setAttribute("contenteditable",!0),s.addEventListener("blur",function(){setTimeout(o.onEditTagBlur.bind(r),0,s)}),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("keydown",function(t){return o.onEditTagkeydown.call(e,t)}),s.focus(),this.setRangeAtStartEnd(!1,s),this.state.editing={scope:i,input:i.querySelector("[contenteditable]")},this.trigger("edit:start",{tag:i,index:n,data:a}),this}else console.warn("Cannot find element in Tag template: ",".tagify__tag-text")},onEditTagDone:function(t,e){var i={tag:t,index:this.getNodeIndex(t),data:e};this.trigger("edit:beforeUpdate",i),this.replaceTag(t,e),this.trigger("edit:updated",i)},replaceTag:function(t,e){var i=this,s=t.querySelector(".tagify__tag-text"),n=s.cloneNode(!0),a=this.getNodeIndex(t);this.state.editing.locked||(this.state.editing={locked:!0},setTimeout(function(){return delete i.state.editing.locked},500),n.removeAttribute("contenteditable"),t.classList.remove("tagify__tag--editable"),s.parentNode.replaceChild(n,s),e&&(n.innerHTML=e.value,n.title=e.value,this.value[a]=e,this.update()))},setRangeAtStartEnd:function(e,i){i=(i=i||this.DOM.input).lastChild||i;var s=document.getSelection();s.rangeCount&&["Start","End"].forEach(function(t){return s.getRangeAt(0)["set"+t](i,e?0:i.length)})},input:{value:"",set:function(t,e){var i=0<arguments.length&&void 0!==t?t:"",s=!(1<arguments.length&&void 0!==e)||e,n=this.settings.dropdown.closeOnSelect;this.input.value=i,s&&(this.DOM.input.innerHTML=i),!i&&n&&setTimeout(this.dropdown.hide.bind(this),20),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},validate:function(){var t=!this.input.value||this.validateTag(this.input.value);"select"==this.settings.mode?this.DOM.scope.classList.toggle("tagify--invalid",!0!==t):this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach(function(t){return 3==t.nodeType&&i.push(t.nodeValue)}),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," ").replace(/^\s+/,"")},autocomplete:{suggest:function(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value||"",i=e.substr(0,this.input.value.length).toLowerCase(),s=e.substring(this.input.value.length);e&&this.input.value&&i==this.input.value.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide.call(this),!0)}}},getNodeIndex:function(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms:function(){return this.DOM.scope.querySelectorAll(".tagify__tag")},getLastTag:function(){var t=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide):not([readonly])");return t[t.length-1]},isTagDuplicate:function(e){var i=this;return"select"!=this.settings.mode&&this.value.some(function(t){return i.isObject(e)?JSON.stringify(t).toLowerCase()===JSON.stringify(e).toLowerCase():e.trim().toLowerCase()===t.value.toLowerCase()})},getTagIndexByValue:function(i){var s=[];return this.getTagElms().forEach(function(t,e){t.textContent.trim().toLowerCase()==i.toLowerCase()&&s.push(e)}),s},getTagElmByValue:function(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},markTagByValue:function(t,e){return!!(e=e||this.getTagElmByValue(t))&&(e.classList.add("tagify--mark"),e)},isTagBlacklisted:function(e){return e=e.toLowerCase().trim(),this.settings.blacklist.filter(function(t){return e==t.toLowerCase()}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){return"string"==typeof e?e.trim().toLowerCase()===(t.value||t).toLowerCase():JSON.stringify(t).toLowerCase()===JSON.stringify(e).toLowerCase()})},validateTag:function(t){var e=t.trim(),i=this.settings,s=!0;return e?i.pattern&&!i.pattern.test(e)?s=this.TEXTS.pattern:!i.duplicates&&this.isTagDuplicate(e)?s=this.TEXTS.duplicate:(this.isTagBlacklisted(e)||i.enforceWhitelist&&!this.isTagWhitelisted(e))&&(s=this.TEXTS.notAllowed):s=this.TEXTS.empty,s},hasMaxTags:function(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},normalizeTags:function(t){function i(t){return t.split(a).filter(function(t){return t}).map(function(t){return{value:t.trim()}})}var e,s=this.settings,n=s.whitelist,a=s.delimiters,o=s.mode,r=!!n&&n[0]instanceof Object,l=t instanceof Array,d=l&&t[0]instanceof Object&&"value"in t[0],c=[];if(d)return t=(e=[]).concat.apply(e,u(t.map(function(e){return i(e.value).map(function(t){return g({},e,{},t)})})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=i(t)}else if(l){var h;t=(h=[]).concat.apply(h,u(t.map(function(t){return i(t)})))}return r&&(t.forEach(function(e){var t=n.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?c.push(t[0]):"mix"!=o&&c.push(e)}),t=c),t},parseMixTags:function(t){var o=this,e=this.settings,r=e.mixTagsInterpolator,l=e.duplicates,d=e.transformTag,c=e.enforceWhitelist;return t=t.split(r[0]).map(function(t,e){var i,s,n=t.split(r[1]),a=n[0];try{i=JSON.parse(a)}catch(t){i=o.normalizeTags(a)[0]}if(!(1<n.length)||c&&!o.isTagWhitelisted(i.value)||!l&&o.isTagDuplicate(i)){if(t)return e?r[0]+t:t}else d.call(o,i),s=o.createTagElem(i),n[0]=s.outerHTML,o.value.push(i);return n.join("")}).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.update(),t},replaceTextWithNode:function(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,n=window.getSelection(),a=n.anchorNode;return a.splitText(n.anchorOffset),i=a.nodeValue.lastIndexOf(e),(s=a.splitText(i)).nodeValue=s.nodeValue.replace(e,""),a.parentNode.insertBefore(t,s),this.DOM.input.normalize(),s}},selectTag:function(t,e){return this.input.set.call(this,e.value,!0),setTimeout(this.setRangeAtStartEnd.bind(this)),this.getLastTag()?this.replaceTag(this.getLastTag(),e):this.appendTag(t),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]},addEmptyTag:function(){var t={value:""},e=this.createTagElem(t);this.appendTag(e),this.value.push(t),this.update(),this.editTag(e)},addTags:function(t,e,i){var s,n=this,a=2<arguments.length&&void 0!==i?i:this.settings.skipInvalid,o=[],r=this.settings;return t&&0!=t.length?(t=this.normalizeTags(t),this.state.editing.scope?this.onEditTagDone(this.state.editing.scope,t[0]):"mix"==r.mode?(r.transformTag.call(this,t[0]),s=this.createTagElem(t[0]),this.replaceTextWithNode(s)||this.DOM.input.appendChild(s),this.DOM.input.appendChild(document.createTextNode("")),t[0].prefix=t[0].prefix||this.state.tag?this.state.tag.prefix:(r.pattern.source||r.pattern)[0],this.value.push(t[0]),this.update(),this.state.tag=null,this.trigger("add",this.extend({},{tag:s},{data:t[0]})),this.DOM.input.appendChild(document.createTextNode("")),s):("select"==r.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach(function(t){var e,i,s={};if(t=Object.assign({},t),r.transformTag.call(n,t),!0!==(e=n.hasMaxTags()||n.validateTag(t.value))){if(a)return;s["aria-invalid"]=!0,s.class=(t.class||"")+" tagify--notAllowed",s.title=e,n.markTagByValue(t.value)}if(s.role="tag",t.readonly&&(s["aria-readonly"]=!0),i=n.createTagElem(n.extend({},t,s)),o.push(i),"select"==r.mode)return n.selectTag(i,t);n.appendTag(i),!0===e?(n.value.push(t),n.update(),n.trigger("add",{tag:i,index:n.value.length-1,data:t})):(n.trigger("invalid",{data:t,index:n.value.length,tag:i,message:e}),r.keepInvalidTags||setTimeout(function(){return n.removeTag(i,!0)},1e3)),n.dropdown.position.call(n)}),t.length&&e&&this.input.set.call(this),this.dropdown.refilter.call(this),o)):("select"==r.mode&&this.removeAllTags(),o)},appendTag:function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)},minify:function(t){return t?t.replace(/\>[\r\n ]+\</g,"><").replace(/(<.*?>)|\s+/g,function(t,e){return e||" "}):""},createTagElem:function(t){var e=this.escapeHTML(t.value),i=this.settings.templates.tag.call(this,e,t);return this.settings.readonly&&(t.readonly=!0),i=this.minify(i),this.parseHTML(i)},removeTag:function(t,e,i){if(t=t||this.getLastTag(),i=i||this.CSSVars.tagHideTransition,"string"==typeof t&&(t=this.getTagElmByValue(t)),t instanceof HTMLElement){var s,n=this,a=this.getNodeIndex(t);"select"==this.settings.mode&&(i=0,this.input.set.call(this)),t.classList.contains("tagify--notAllowed")&&(e=!0),i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(o,i)):o()}function o(){t.parentNode&&(t.parentNode.removeChild(t),e?n.settings.keepInvalidTags&&n.trigger("remove",{tag:t,index:a}):(s=n.value.splice(a,1)[0],n.update(),n.trigger("remove",{tag:t,index:a,data:s}),n.dropdown.refilter.call(n),n.dropdown.position.call(n)))}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.getTagElms()).forEach(function(t){return t.parentNode.removeChild(t)}),this.dropdown.position.call(this),"select"==this.settings.mode&&this.input.set.call(this)},preUpdate:function(){this.DOM.scope.classList.toggle("tagify--hasMaxTags",this.value.length>=this.settings.maxTags),this.DOM.scope.classList.toggle("tagify--noTags",!this.value.length)},update:function(){this.preUpdate(),this.DOM.originalInput.value="mix"==this.settings.mode?this.getMixedTagsAsString():this.value.length?JSON.stringify(this.value):""},getMixedTagsAsString:function(){var e=this,i="",s=0,n=this.settings.mixTagsInterpolator;return this.DOM.input.childNodes.forEach(function(t){1==t.nodeType&&t.classList.contains("tagify__tag")?i+=n[0]+JSON.stringify(e.value[s++])+n[1]:i+=t.textContent}),i},getNodeHeight:function(t){var e,i=t.cloneNode(!0);return i.style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(".tagify__dropdown__wrapper")},build:function(){var t=this.settings.dropdown,e=t.position,i=t.classname,s="".concat("manual"==e?"":"tagify__dropdown tagify__dropdown--".concat(e)," ").concat(i).trim();return this.parseHTML('<div class="'.concat(s,'" role="listbox" aria-labelledby="dropdown">\n                        <div class="tagify__dropdown__wrapper"></div>\n                    </div>'))},show:function(t){var e,i,s,n,a=this,o=this.settings,r="manual"==o.dropdown.position;if(o.whitelist&&o.whitelist.length&&!1!==o.dropdown.enable){if(this.suggestedListItems=this.dropdown.filterListItems.call(this,t),!this.suggestedListItems.length)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide.call(this);s=(i=this.suggestedListItems[0]).value||i,o.autoComplete&&0==s.indexOf(t)&&this.input.autocomplete.suggest.call(this,i),e=this.dropdown.createListHTML.call(this,this.suggestedListItems),this.DOM.dropdown.content.innerHTML=this.minify(e),(o.enforceWhitelist&&!r||o.dropdown.highlightFirst)&&this.dropdown.highlightOption.call(this,this.DOM.dropdown.content.children[0]),this.DOM.scope.setAttribute("aria-expanded",!0),this.trigger("dropdown:show",this.DOM.dropdown),this.state.dropdown.visible=t||!0,this.dropdown.position.call(this),document.body.contains(this.DOM.dropdown)||(r||(this.events.binding.call(this,!1),n=this.getNodeHeight(this.DOM.dropdown),this.DOM.dropdown.classList.add("tagify__dropdown--initial"),this.dropdown.position.call(this,n),document.body.appendChild(this.DOM.dropdown),setTimeout(function(){return a.DOM.dropdown.classList.remove("tagify__dropdown--initial")})),setTimeout(this.dropdown.events.binding.bind(this)))}},hide:function(t){var e=this.DOM,i=e.scope,s=e.dropdown,n="manual"==this.settings.dropdown.position&&!t;s&&document.body.contains(s)&&!n&&(window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),setTimeout(this.events.binding.bind(this),250),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),this.state.dropdown.visible=!1,this.state.ddItemData=null,this.state.ddItemElm=null,this.trigger("dropdown:hide",s))},refilter:function(){this.suggestedListItems=this.dropdown.filterListItems.call(this,"");var t=this.dropdown.createListHTML.call(this,this.suggestedListItems);this.DOM.dropdown.content.innerHTML=this.minify(t)},position:function(t){var e,i,s,n,a,o,r=this.DOM.dropdown;this.state.dropdown.visible&&(o="text"==this.settings.dropdown.position?(n=(i=this.getCaretGlobalPosition()).bottom,s=i.top,a=i.left,"auto"):(s=(i=this.DOM.scope.getBoundingClientRect()).top,n=i.bottom-1,a=i.left,i.width+"px"),s=Math.floor(s),n=Math.ceil(n),e=document.documentElement.clientHeight-n<(t||r.clientHeight),r.style.cssText="left:"+(a+window.pageXOffset)+"px; width:"+o+";"+(e?"bottom:"+(document.documentElement.clientHeight-s-window.pageYOffset-2)+"px;":"top: "+(n+window.pageYOffset)+"px"),r.setAttribute("placement",e?"top":"bottom"))},events:{binding:function(t){var e=!(0<arguments.length&&void 0!==t)||t,i=this.dropdown.events.callbacks,s=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:i.onKeyDown.bind(this),onMouseOver:i.onMouseOver.bind(this),onMouseLeave:i.onMouseLeave.bind(this),onClick:i.onClick.bind(this)},n=e?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(window[n]("resize",s.position),window[n]("keydown",s.onKeyDown)),this.DOM.dropdown[n]("mouseover",s.onMouseOver),this.DOM.dropdown[n]("mouseleave",s.onMouseLeave),this.DOM.dropdown[n]("mousedown",s.onClick),this.DOM[this.listeners.main.click[0]][n]("click",this.listeners.main.click[1])},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelector("[class$='--active']"),i=e;switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":var s;t.preventDefault(),i=(i=i&&i[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"])||(s=this.DOM.dropdown.content.children)["ArrowUp"==t.key||"Up"==t.key?s.length-1:0],this.dropdown.highlightOption.call(this,i,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":if(t.preventDefault(),"mix"!=this.settings.mode&&!this.settings.autoComplete.rightKey){try{var n=i?i.textContent:this.suggestedListItems[0].value;this.input.autocomplete.set.call(this,n)}catch(t){}return!1}case"Enter":t.preventDefault(),this.dropdown.selectOption.call(this,e);break;case"Backspace":if("mix"==this.settings.mode||this.state.editing.scope)return;var a=this.input.value.trim();""!=a&&8203!=a.charCodeAt(0)||(!0===this.settings.backspace?this.removeTag():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}},onMouseOver:function(t){var e=t.target.closest(".tagify__dropdown__item");e&&this.dropdown.highlightOption.call(this,e)},onMouseLeave:function(){this.dropdown.highlightOption.call(this)},onClick:function(t){if(0==t.button&&t.target!=this.DOM.dropdown){var e=t.target.closest(".tagify__dropdown__item");this.dropdown.selectOption.call(this,e)}}}},highlightOption:function(t,e){var i,s="tagify__dropdown__item--active";if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,(this.state.ddItemElm=t).classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),"manual"!=this.settings.dropdown.position&&this.dropdown.position.call(this))},selectOption:function(t){var e=this;if(t){this.state.actions.selectOption=!0,setTimeout(function(){return e.state.actions.selectOption=!1},50);var i=this.settings.dropdown.closeOnSelect,s=this.suggestedListItems[this.getNodeIndex(t)]||this.input.value;this.trigger("dropdown:select",s),this.addTags([s],!0),setTimeout(function(){e.DOM.input.focus(),e.toggleFocusClass(!0)}),i&&this.dropdown.hide.call(this)}},filterListItems:function(t){var i,e,s,n,a=this,o=this.settings,r=[],l=o.whitelist,d=o.dropdown.maxItems||1/0,c=o.dropdown.searchKeys.concat(["searchBy","value"]),h=0;if(!t)return(o.duplicates?l:l.filter(function(t){return!a.isTagDuplicate(t.value||t)})).slice(0,d);for(;h<l.length&&(i=l[h]instanceof Object?l[h]:{value:l[h]},s=c.reduce(function(t,e){return t+" "+(i[e]||"")},"").toLowerCase().indexOf(t.toLowerCase()),e=o.dropdown.fuzzySearch?0<=s:0==s,n=!o.duplicates&&this.isTagDuplicate(i.value),e&&!n&&d--&&r.push(i),0!=d);h++);return r},createListHTML:function(t){var e=this.settings.templates.dropdownItem.bind(this);return this.minify(t.map(e).join(""))}}},t});
\ No newline at end of file
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s<arguments.length;s++){var a=null!=arguments[s]?arguments[s]:{};s%2?t(Object(a),!0).forEach((function(t){i(e,t,a[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(a)):t(Object(a)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(a,t))}))}return e}function i(t,e,i){return e in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}const s=(t,e,i,s)=>(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase());function a(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function n(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function o(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function r(t){return"string"==typeof t?t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/`|'/g,"&#039;"):t}function l(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function d(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(l(e[i])){l(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function h(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var g=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function c(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}var p={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",scopeLoading:"tagify--loading",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty",inputInvalid:"tagify__input--invalid",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",tag:"tagify__tag",tagText:"tagify__tag-text",tagX:"tagify__tag__removeBtn",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}};function u(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var m={refs(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector)},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,d="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,(!r||o||n.templates.dropdownItemNoMatch)&&!1!==n.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(l(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),d||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return"number"==typeof a.dropdown.enabled&&a.dropdown.enabled>=0?(this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this):this},fill(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\</g,"><").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=document.documentElement.clientHeight,g=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?e.position:"all",c=this.DOM["input"==g?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==g?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=c.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),d=void 0===d?h-i.bottom<t:d,l.style.cssText="left:"+(n+window.pageXOffset)+"px; width:"+o+";"+(d?"top: "+(s+window.pageYOffset)+"px":"top: "+(a+window.pageYOffset)+"px"),l.setAttribute("placement",d?"top":"bottom"),l.setAttribute("position",g))}},events:{binding(t=!0){var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown(t){var e=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownItemActiveSelector),i=this.dropdown.getSuggestionDataByNode(e);switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":var s;t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(s=this.DOM.dropdown.content.children,e=s["ArrowUp"==t.key||"Up"==t.key?s.length-1:0]),i=this.dropdown.getSuggestionDataByNode(e),this.dropdown.highlightOption(e,!0);break;case"Escape":case"Esc":this.dropdown.hide();break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":if("mix"!=this.settings.mode&&e&&!this.settings.autoComplete.rightKey&&!this.state.editing){t.preventDefault();var a=this.dropdown.getMappedValue(i);return this.input.autocomplete.set.call(this,a),!1}return!0;case"Enter":t.preventDefault(),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{if(e)return this.dropdown.selectOption(e);this.dropdown.hide(),"mix"!=this.settings.mode&&this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t?+t.getAttribute("tagifySuggestionIdx"):-1;return this.suggestedListItems[e]||null},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t){var e=this.settings.dropdown,i=e.clearOnSelect,s=e.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(s&&this.dropdown.hide());var a=t.getAttribute("tagifySuggestionIdx"),n=this.suggestedListItems[+a];this.trigger("dropdown:select",{data:n,elm:t}),a&&n?(this.state.editing?this.onEditTagDone(null,d({__isValid:!0},this.normalizeTags([n])[0])):this["mix"==this.settings.mode?"addMixTags":"addTags"]([n],i),this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),s?setTimeout(this.dropdown.hide.bind(this)):this.dropdown.refilter())):this.dropdown.hide()},selectAll(){return this.suggestedListItems.length=0,this.dropdown.hide(),this.addTags(this.dropdown.filterListItems(""),!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,d=r.dropdown,g=(e=e||{},t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t,[]),c=[],p=r.whitelist,u=d.maxItems||1/0,m=d.searchKeys,v=0;if(!t||!m.length)return(r.duplicates?p:p.filter((t=>!this.isTagDuplicate(l(t)?t.value:t)))).slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=d.caseSensitive?""+t:(""+t).toLowerCase();v<p.length;v++){let t,u;i=p[v]instanceof Object?p[v]:{value:p[v]};let T=!Object.keys(i).some((t=>m.includes(t)))?["value"]:m;d.fuzzySearch&&!e.exact?(a=T.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),d.accentedSearch&&(a=h(a),o=h(o)),t=0==a.indexOf(o),u=a===o,s=f(a,o)):(t=!0,s=T.some((t=>{var s=""+(i[t]||"");return d.accentedSearch&&(s=h(s),o=h(o)),d.caseSensitive||(s=s.toLowerCase()),u=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!r.duplicates&&this.isTagDuplicate(l(i)?i.value:i),s&&!n&&(u&&t?c.push(i):"startsWith"==d.sortby&&t?g.unshift(i):g.push(i))}return"function"==typeof d.sortby?d.sortby(c.concat(g),o):c.concat(g).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return d([],t).map(((t,e)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var i=this.dropdown.getMappedValue(t);t.value=i&&"string"==typeof i?r(i):i;var s=this.settings.templates.dropdownItem.apply(this,[t,this]);return s=s.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gim,"").replace(">",` tagifySuggestionIdx="${e}">`)})).join("")}};const v="@yaireo/tagify/";var f,T={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},w={wrapper:(t,e)=>`<tags class="${e.classNames.namespace} ${e.mode?`${e.classNames[e.mode+"Mode"]}`:""} ${t.className}"\n                    ${e.readonly?"readonly":""}\n                    ${e.disabled?"disabled":""}\n                    ${e.required?"required":""}\n                    tabIndex="-1">\n            <span ${!e.readonly&&e.userInput?"contenteditable":""} tabIndex="0" data-placeholder="${e.placeholder||"&#8203;"}" aria-placeholder="${e.placeholder||""}"\n                class="${e.classNames.input}"\n                role="textbox"\n                aria-autocomplete="both"\n                aria-multiline="${"mix"==e.mode}"></span>\n                &#8203;\n        </tags>`,tag(t,e){var i=this.settings;return`<tag title="${t.title||t.value}"\n                    contenteditable='false'\n                    spellcheck='false'\n                    tabIndex="${i.a11y.focusableTags?0:-1}"\n                    class="${i.classNames.tag} ${t.class||""}"\n                    ${this.getAttributes(t)}>\n            <x title='' class="${i.classNames.tagX}" role='button' aria-label='remove tag'></x>\n            <div>\n                <span class="${i.classNames.tagText}">${t[i.tagTextProp]||t.value}</span>\n            </div>\n        </tag>`},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return`<div class="${i?"":s} ${e.classname}" role="listbox" aria-labelledby="dropdown">\n                    <div class="${t.classNames.dropdownWrapper}"></div>\n                </div>`},dropdownItem(t,e){return`<div ${this.getAttributes(t)}\n                    class='${this.settings.classNames.dropdownItem} ${t.class?t.class:""}'\n                    tabindex="0"\n                    role="option">${t.value}</div>`},dropdownItemNoMatch:null};var b={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(t=!0){var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)],drop:["input",i.onDrop.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n&&n.disconnect(),"mix"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(t||!this.listeners.global)for(e of(this.listeners.global=this.listeners&&this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=t.relatedTarget&&c.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0!==i.dropdown.enabled&&i.userInput||this.dropdown.show(this.value.length?"":void 0));"blur"==s&&(this.trigger("blur",n),this.loading(!1),"select"==this.settings.mode&&l&&(e=""),("select"==this.settings.mode&&e?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0),"select"!=this.settings.mode||e||this.removeTags()),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onWindowKeyDown(t){var e,i=document.activeElement;if(c.call(this,i)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s,a,r,l=document.getSelection(),d="Delete"==t.key&&l.anchorOffset==(l.anchorNode.length||0),h=l.anchorNode.previousSibling,p=1==l.anchorNode.nodeType||!l.anchorOffset&&h&&1==h.nodeType&&l.anchorNode.previousSibling,u=n(this.DOM.input.innerHTML),m=this.getTagElms();if("edit"==e.backspace&&p)return s=1==l.anchorNode.nodeType?null:l.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,s),void t.preventDefault();if(g()&&p)return r=o(p),p.hasAttribute("readonly")||p.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(r),this.DOM.input.click()}));if("BR"==l.anchorNode.nodeName)return;if((d||p)&&1==l.anchorNode.nodeType?a=0==l.anchorOffset?d?m[0]:null:m[l.anchorOffset-1]:d?a=l.anchorNode.nextElementSibling:p&&(a=p),3==l.anchorNode.nodeType&&!l.anchorNode.nodeValue&&l.anchorNode.previousElementSibling&&t.preventDefault(),(p||d)&&!e.backspace)return void t.preventDefault();if("Range"!=l.type&&!l.anchorOffset&&l.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=l.type&&a&&a.hasAttribute("readonly"))return void this.placeCaretAfterNode(o(a));clearTimeout(f),f=setTimeout((()=>{var t=document.getSelection(),e=n(this.DOM.input.innerHTML),i=!d&&t.anchorNode.previousSibling;if(e.length>=u.length&&i)if(c.call(this,i)&&!i.hasAttribute("readonly")){if(this.removeTags(i),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName)return this.DOM.input.innerHTML="",this.value.length=0,!0}else i.remove();this.value=[].map.call(m,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(i,!0)}))}},onInput(t){if(this.postUpdate(),"mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.state.inputText!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"](e),this.trigger("input",s))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,h=this.settings,c=this.value.length,p=this.getTagElms(),u=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&g()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&u.appendChild(this.createTagElem(t))})),u.childNodes.length&&(m.insertNode(u),this.setRangeAtStartEnd(!1,u.lastChild)),p.length!=c)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(h.pattern).length-1,(i=e.match(h.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(h.pattern)[0],value:a.replace(h.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(h.delimiters))return this.state.tag.value=this.state.tag.value.replace(h.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,h.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=h.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s<this.state.mixMode.matchedPatternCount)&&(n=!1)}else this.state.flaggedTags={};this.state.mixMode.matchedPatternCount=s}setTimeout((()=>{this.update({withoutChangeEvent:!0}),this.trigger("input",d({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),originalEvent:this.cloneEvent(t)}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show(this.value.length?"":void 0):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show());this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if("select"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):this.state.inputText=e)})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=i.innerHTML!=i.__tagifyTagData.__originalHTML,r=this.validateTag({[this.settings.tagTextProp]:n});o||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:d({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i,s=this.settings,a=t.closest("."+s.classNames.tag),n=this.input.normalize.call(this,t),o=this.tagData(a).__originalData,r=a.innerHTML!=a.__tagifyTagData.__originalHTML,l=this.validateTag({[s.tagTextProp]:n});if(n)if(r){if(e=this.hasMaxTags(),i=this.getWhitelistItem(n)||d({},o,{[s.tagTextProp]:n,value:n,__isValid:l}),s.transformTag.call(this,i,o),!0!==(l=(!e||!0===o.__isValid)&&this.validateTag({[s.tagTextProp]:i[s.tagTextProp]}))){if(this.trigger("invalid",{data:i,tag:a,message:l}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=l:i=o}else s.keepInvalidTags&&(delete i.title,delete i["aria-invalid"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,o);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":e.innerHTML=e.__tagifyTagData.__originalHTML;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&a.userInput&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if(t)if("<div><br></div>"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),this.placeCaretAfterNode(e.previousSibling)}else c.call(this,t)&&t.previousSibling&&"BR"==t.previousSibling.nodeName&&(t.previousSibling.replaceWith("\n​"),this.placeCaretAfterNode(t.previousSibling.previousSibling))})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&c.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function y(t,e){if(!t){console.warn("Tagify:","input element not found",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify"))return console.warn("Tagify: ","input element is already Tagified",t),this;var i;d(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?d({},o):o).tagify=this,s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s="/"+t;if(1==localStorage.getItem(v+i+"/v",1))try{e=JSON.parse(localStorage[v+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(v+t+"/v",1),(e,i)=>{let s="/"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(v+t+s,a),dispatchEvent(new Event("storage")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=v+"/"+t+"/";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:"",editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),u.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus()}return y.prototype={_dropdown:m,customEventsList:["change","add","remove","invalid","input","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"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:w,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],"whitelist")},get whitelist(){return this.settings.whitelist},applySettings(t,i){p.templates=this.templates;var s=this.settings=d({},p,i);s.disabled=t.hasAttribute("disabled"),s.readonly=s.readonly||t.hasAttribute("readonly"),s.placeholder=r(t.getAttribute("placeholder")||s.placeholder||""),s.required=t.hasAttribute("required");for(let t in s.classNames)Object.defineProperty(s.classNames,t+"Selector",{get(){return"."+this[t].split(" ")[0]}});if(this.isIE&&(s.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(s.delimiters))instanceof Array&&(s[e]=i)})),"autoComplete"in i&&!l(i.autoComplete)&&(s.autoComplete=p.autoComplete,s.autoComplete.enabled=i.autoComplete),"mix"==s.mode&&(s.autoComplete.rightKey=!0,s.delimiters=i.delimiters||null,s.tagTextProp&&!s.dropdown.searchKeys.includes(s.tagTextProp)&&s.dropdown.searchKeys.push(s.tagTextProp)),t.pattern)try{s.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{s.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}s.disabled&&(s.userInput=!1),this.TEXTS=e(e({},T),s.texts||{}),"select"!=s.mode&&s.userInput||(s.dropdown.enabled=0),s.dropdown.appendTarget=i.dropdown&&i.dropdown.appendTarget?i.dropdown.appendTarget:document.body;let a=this.getPersistedData("whitelist");Array.isArray(a)&&(this.whitelist=Array.isArray(s.whitelist)?function(){const t=[],e={};for(let i of arguments)for(let s of i)l(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}(s.whitelist,a):a)},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!l(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=r(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(({value:t,unit:e})=>"s"==e?1e3*t:t)(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t))},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData("value");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if("mix"==i.mode)this.parseMixTags(this.trim(t)),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","<br>");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.blockChangeEvent=!1},cloneEvent(t){var e={};for(var i in t)e[i]=t[i];return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:b,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e||t),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings;function s(){return t.querySelector(i.classNames.tagTextSelector)}var a=s(),n=this.getNodeIndex(t),o=this.tagData(t),r=this.events.callbacks,l=this,h=!0;if(a){if(!(o instanceof Object&&"editable"in o)||o.editable)return a.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),this.tagData(t,{__originalData:d({},o),__originalHTML:t.innerHTML}),a.addEventListener("focus",r.onEditTagFocus.bind(this,t)),a.addEventListener("blur",(function(){setTimeout((()=>r.onEditTagBlur.call(l,s())))})),a.addEventListener("input",r.onEditTagInput.bind(this,a)),a.addEventListener("keydown",(e=>r.onEditTagkeydown.call(this,e,t))),a.focus(),this.setRangeAtStartEnd(!1,a),e.skipValidation||(h=this.editTagToggleValidity(t)),a.originalIsValid=h,this.trigger("edit:start",{tag:t,index:n,data:o,isValid:h}),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||this.tagData(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),this.settings.a11y.focusableTags?t.focus():this.placeCaretAfterNode(t.previousSibling)):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&d(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){}},injectAtCaret(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this},input:{set(t="",e=!0){var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=r(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(...t){var e="."+[...this.settings.classNames.tag.split(" "),...t].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(e))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},tagData:(t,e,i)=>t?(e&&(t.__tagifyTagData=i?e:d({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag element doesn't exist",t,e),e),isTagDuplicate(t,e){var i=this.settings;return"select"!=i.mode&&this.value.reduce(((a,n)=>s(this.trim(""+t),n.value,e||i.dropdown.caseSensitive)?a+1:a),0)},getTagIndexByValue(t){var e=[];return this.getTagElms().forEach(((i,a)=>{s(this.trim(i.textContent),t,this.settings.dropdown.caseSensitive)&&e.push(a)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,this.state.editing)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),"mix"==i.mode&&this.setContentEditable(!t)},setContentEditable(t){!this.settings.readonly&&this.settings.userInput&&(this.DOM.input.contentEditable=t)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=t instanceof Array,d=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=d(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:d(t)))));return r&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,c=t.split(i[1]),p=c[0],u=l.length==o;try{if(p==+p)throw Error;h=JSON.parse(p)}catch(t){h=this.normalizeTags(p)[0]||{value:p}}if(a.call(this,h),u||!(c.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),c[0]=g.outerHTML,this.value.push(h);return c.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))?!0:(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0)}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),i.enforceWhitelist&&this.setContentEditable(!1),this.value[0]=e,this.update(),this.trigger("add",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=d({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=document.createDocumentFragment();return i=i||a.skipInvalid,t&&0!=t.length?(t=this.normalizeTags(t),"mix"==a.mode?this.addMixTags(t):("select"==a.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,o={},r=Object.assign({},t,{value:t.value+""});if(t=Object.assign({},r),a.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(i)return;d(o,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:r}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value))}if("readonly"in t&&(t.readonly?o["aria-readonly"]=!0:delete t.readonly),e=this.createTagElem(t,o),s.push(e),"select"==a.mode)return this.selectTag(e,t);n.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(n),this.update(),t.length&&e&&this.input.set.call(this),this.dropdown.refilter(),s)):("select"==a.mode&&this.removeAllTags(),s)},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);"string"==typeof t&&(t=[{value:t}]);var e=!!this.state.selection,i=document.createDocumentFragment();return t.forEach((t=>{var e=this.createTagElem(t);i.appendChild(e),this.insertAfterTag(e)})),e?this.injectAtCaret(i):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(i),this.updateValueByDOMTags(),this.update()),i},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",d({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.scope.lastElementChild;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,i){t.__tagId=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)));var s,a=d({},t,e({value:r(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a])),this.tagData(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((t,e)=>{var i=this.tagData(t),s=this.hasMaxTags(),a=this.validateTag(i);if(!0===a&&!s)return i=i.__preInvalidData?i.__preInvalidData:{value:i.value},this.replaceTag(t,i);t.title=s||a}))},removeTags(t,e,i){var s;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&"string"==typeof e&&(e=this.getTagElmByValue(e));var i=this.tagData(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:this.tagData(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length)return this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==this.settings.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=this.tagData(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.position(),"select"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.settings.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.toggleClass(t.hasNoTags,!this.value.length),this.toggleClass(t.empty,!e)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,"value"))},update(t){var e=this.getInputValue();this.setOriginalInputValue(e),this.postUpdate(),(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return e=t||this.value,i=this.dataProps,e&&Array.isArray(e)&&e.map((t=>a(t,i)));var e,i},getMixedTagsAsString(){var t="",e=this,i=this.settings.mixTagsInterpolator;return function s(n){n.childNodes.forEach((n=>{if(1==n.nodeType){const o=e.tagData(n);if("BR"==n.tagName&&(t+="\r\n"),"DIV"==n.tagName||"P"==n.tagName)t+="\r\n",s(n);else if(c.call(e,n)&&o){if(o.__removed)return;t+=i[0]+JSON.stringify(a(o,e.dataProps))+i[1]}}else t+=n.textContent}))}(this.DOM.input),t}},y.prototype.removeTag=y.prototype.removeTags,y}));