diff --git a/dist/tagify.js b/dist/tagify.js
index 0ef94cff..20b24b71 100644
--- a/dist/tagify.js
+++ b/dist/tagify.js
@@ -803,12 +803,12 @@ Tagify.prototype = {
isValid = this.validateTag(tagData.value);
} else {
- this.replaceTag(tagElm);
+ this.onEditTagDone(tagElm);
return;
}
if (isValid !== undefined && isValid !== true) return;
- this.replaceTag(tagElm, tagData);
+ this.onEditTagDone(tagElm, tagData);
},
onEditTagkeydown: function onEditTagkeydown(e) {
this.trigger("edit:keydown", {
@@ -881,6 +881,16 @@ Tagify.prototype = {
});
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);
+ },
/**
* Exit a tag's edit-mode.
@@ -891,7 +901,7 @@ Tagify.prototype = {
var editableElm = tagElm.querySelector('.tagify__tag-text'),
clone = editableElm.cloneNode(true),
- tagElmIdx;
+ 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.
@@ -911,14 +921,8 @@ Tagify.prototype = {
clone.innerHTML = tagData.value;
clone.title = tagData.value; // update data
- tagElmIdx = this.getNodeIndex(tagElm);
this.value[tagElmIdx] = tagData;
this.update();
- this.trigger("edit:updated", {
- tag: tagElm,
- index: tagElmIdx,
- data: tagData
- });
}
},
@@ -1240,7 +1244,6 @@ Tagify.prototype = {
}).join('');
this.DOM.input.innerHTML = s;
this.DOM.input.appendChild(document.createTextNode(''));
- this.DOM.input.appendChild(document.createTextNode(''));
this.update();
return s;
},
@@ -1263,8 +1266,7 @@ Tagify.prototype = {
// get index of last occurence of "#ba"
idx = nodeAtCaret.nodeValue.lastIndexOf(tagString);
- replacedNode = nodeAtCaret.splitText(idx); // #ba
- // clean up the tag's string and put tag element instead
+ 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);
@@ -1322,16 +1324,15 @@ Tagify.prototype = {
if (!tagsItems || tagsItems.length == 0) {
// is mode is "select" clean all tags
- if (_s.mode == 'select') this.removeAllTags(); // console.warn('[addTags]', 'no tags to add:', tagsItems)
-
+ if (_s.mode == 'select') this.removeAllTags();
return tagElems;
- }
+ } // converts Array/String/Object to an Array of Objects
+
- tagsItems = this.normalizeTags(tagsItems); // converts Array/String/Object to an Array of Objects
- // if in edit-mode, do not continue but instead replace the tag's text
+ tagsItems = this.normalizeTags(tagsItems); // if in edit-mode, do not continue but instead replace the tag's text
if (this.state.editing.scope) {
- return this.replaceTag(this.state.editing.scope, tagsItems[0]);
+ return this.onEditTagDone(this.state.editing.scope, tagsItems[0]);
}
if (_s.mode == 'mix') {
diff --git a/dist/tagify.min.js b/dist/tagify.min.js
index b1dd6684..1cebb24d 100644
--- a/dist/tagify.min.js
+++ b/dist/tagify.min.js
@@ -4,1960 +4,4 @@
* Don't sell this code. (c)
* https://github.com/yairEO/tagify
*/
-/**
- * Tagify (v 3.2.4)- 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();
- }
-}(this, function() {
-"use strict";
-
-function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
-
-function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
-
-function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
-
-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; } }
-
-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; }
-
-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; }
-
-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; }
-
-/**
- * @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;
- }
-
- 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: {
- delimiters: ",",
- // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |."
- pattern: null,
- // RegEx pattern to validate input by. Ex: /[1-9]/
- maxTags: Infinity,
- // Maximum number of tags
- callbacks: {},
- // Exposed callbacks object to be triggered on certain events
- addTagOnBlur: true,
- // Flag - automatically adds the text which was inputed as a tag when blur event happens
- duplicates: false,
- // Flag - allow tuplicate 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
- 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
- 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,
- // 1 or 2 clicks to edit a tag
- transformTag: function transformTag() {},
- // Takes a tag input string as argument and returns a transformed value
- 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"
-
- },
- dropdown: {
- classname: '',
- enabled: 2,
- // minimum input characters needs to be typed for the dropdown to show
- maxItems: 10,
- searchKeys: [],
- fuzzySearch: 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'
-
- }
- },
- // 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 "\n \n ");
- },
- tag: function tag(value, tagData) {
- return "\n \n \n ").concat(value, "\n
\n ");
- },
- 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, "'");
- return "
").concat(sanitizedValue, "
");
- }
- },
- 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;
- }
-
- if (input.pattern) try {
- this.settings.pattern = new RegExp(input.pattern);
- } catch (e) {} // Convert the "delimiters" setting into a REGEX object
-
- 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)
-
-
- if (this.settings.mode == 'select') this.settings.dropdown.enabled = 0;
- if (this.settings.mode == 'mix') this.settings.autoComplete.rightKey = true;
- },
-
- /**
- * 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], "\"") : "");
- }
-
- 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;
- },
-
- /**
- * 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;
- },
-
- /**
- * 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
- };
- }
- }
-
- 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.CSSVars = {
- tagHideTransition: function (_ref) {
- var value = _ref.value,
- unit = _ref.unit;
- return 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: 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);
- }
- },
-
- /**
- * 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)
-
- if (!value) return;
- this.removeAllTags();
- if (this.settings.mode == 'mix') this.parseMixTags(value.trim());else {
- try {
- value = JSON.parse(value);
- } catch (err) {}
-
- this.addTags(value).forEach(function (tag) {
- return tag && tag.classList.add('tagify--noAnim');
- });
- }
- },
-
- /**
- * 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';
- },
-
- /**
- * 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);
-
- 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];
- }
- }
- }
-
- return o;
- },
- cloneEvent: function cloneEvent(e) {
- var clonedEvent = {};
-
- for (var v in e) {
- clonedEvent[v] = e[v];
- }
-
- return clonedEvent;
- },
-
- /**
- * A constructor for exposing events to the outside
- */
- EventDispatcher: function EventDispatcher(instance) {
- // Create a DOM EventTarget object
- var target = document.createTextNode('');
-
- 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
-
-
- 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;
- };
-
- this.trigger = function (eventName, data) {
- var e;
- if (!eventName) return;
-
- if (instance.settings.isJQueryPlugin) {
- if (eventName == 'remove') eventName = 'removeTag'; // issue #222
-
- jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data]);
- } else {
- try {
- e = new CustomEvent(eventName, {
- "detail": this.extend({}, data, {
- tagify: this
- })
- });
- } catch (err) {
- console.warn(err);
- }
-
- target.dispatchEvent(e);
- }
- };
- },
-
- /**
- * Toogle loading state on/off
- * @param {Boolean} isLoading
- */
- loading: function loading(isLoading) {
- this.DOM.scope.classList.toggle('tagify--loading', isLoading);
- return this;
- },
- toggleFocusClass: function toggleFocusClass(force) {
- this.DOM.scope.classList.toggle('tagify--focus', !!force);
- },
-
- /**
- * DOM events listeners binding
- */
- events: {
- // bind custom events which were passed in the settings
- customBinding: function customBinding() {
- var _this3 = this;
-
- this.customEventsList.forEach(function (name) {
- _this3.on(name, _this3.settings.callbacks[name]);
- });
- },
- binding: function binding() {
- var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
-
- var _CB = this.events.callbacks,
- _CBR,
- action = bindUnbind ? 'addEventListener' : 'removeEventListener',
- editTagsEventType = this.settings.editTags == 1 ? "click_" // TODO: Refactor this crappy hack to allow same event more than once
- : this.settings.editTags == 2 ? "dblclick" : ""; // do not allow the main events to be bound more than once
-
-
- 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;
-
- 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));
- 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 || _defineProperty({
- focus: ['input', _CB.onFocusBlur.bind(this)],
- blur: ['input', _CB.onFocusBlur.bind(this)],
- keydown: ['input', _CB.onKeydown.bind(this)],
- click: ['scope', _CB.onClickScope.bind(this)]
- }, editTagsEventType, ['scope', _CB.onDoubleClickScope.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.replace(/_/g, ''), _CBR[eventName][1]);
- }
- },
-
- /**
- * DOM events callbacks
- */
- callbacks: {
- onFocusBlur: function onFocusBlur(e) {
- var text = e.target ? e.target.textContent.trim() : '',
- // 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;
-
- if (type == 'blur' && e.relatedTarget === this.DOM.scope) {
- this.dropdown.hide.call(this);
- this.DOM.input.focus();
- return;
- }
-
- if (this.state.actions.selectOption && (_s.dropdown.enabled || !_s.dropdown.closeOnSelect)) 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);
- return;
- }
-
- if (type == "focus") {
- this.trigger("focus", {
- relatedTarget: e.relatedTarget
- }); // e.target.classList.remove('placeholder');
-
- if (_s.dropdown.enabled === 0 && _s.mode != "select") {
- this.dropdown.show.call(this);
- }
-
- 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)
-
- text && !this.state.actions.selectOption && _s.addTagOnBlur && this.addTags(text, true);
- }
-
- this.DOM.input.removeAttribute('style');
- this.dropdown.hide.call(this);
- },
- onKeydown: function onKeydown(e) {
- var _this4 = this;
-
- var s = e.target.textContent.trim(),
- tags;
- this.trigger("keydown", {
- originalEvent: this.cloneEvent(e)
- });
-
- if (this.settings.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
- // because it seems likely the user wishes to use the arrows to move the caret
- this.state.actions.ArrowLeft = true;
- break;
- }
-
- 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
-
- tags = this.DOM.input.children; // a minimum delay is needed before the node actually gets ditached 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 () {
- // 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;
- });
- });
- break;
- }
- // currently commented to allow new lines in mixed-mode
- // case 'Enter' :
- // e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
- }
-
- return true;
- }
-
- 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)
- }
-
- break;
-
- case 'Esc':
- case 'Escape':
- if (this.state.dropdown.visible) return;
- e.target.blur();
- break;
-
- case 'Down':
- case 'ArrowDown':
- // if( this.settings.mode == 'select' ) // issue #333
- if (!this.state.dropdown.visible) this.dropdown.show.call(this);
- break;
-
- case 'ArrowRight':
- {
- var tagData = this.state.inputSuggestion || this.state.ddItemData;
-
- if (tagData && this.settings.autoComplete.rightKey) {
- this.addTags([tagData], true);
- return;
- }
-
- break;
- }
-
- case 'Tab':
- {
- if (!s) return true;
- }
-
- case 'Enter':
- 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);
- });
- }
- },
- onInput: function onInput(e) {
- var value = this.settings.mode == 'mix' ? this.DOM.input.textContent : this.input.normalize.call(this),
- showSuggestions = value.length >= this.settings.dropdown.enabled,
- data = {
- value: 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;
- }
-
- 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
-
- this.input.set.call(this, value, false); // update the input with the normalized value and run validations
- // this.setRangeAtStartEnd(); // fix caret position
-
- if (value.search(this.settings.delimiters) != -1) {
- if (this.addTags(value)) {
- 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);
- }
- },
- onMixTagsInput: function onMixTagsInput(e) {
- var _this5 = this;
-
- var sel,
- range,
- split,
- tag,
- showSuggestions,
- _s = this.settings;
- if (this.hasMaxTags()) return true;
-
- if (window.getSelection) {
- sel = window.getSelection();
-
- if (sel.rangeCount > 0) {
- range = sel.getRangeAt(0).cloneRange();
- range.collapse(true);
- range.setStart(window.getSelection().focusNode, 0);
- split = range.toString().split(_s.mixTagsAllowedAfter); // ["foo", "bar", "@a"]
-
- tag = split[split.length - 1].match(_s.pattern);
-
- 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]
- };
- showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled;
- }
- }
- }
-
- this.update(); // 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);
- }, 10);
- },
- onInputIE: function onInputIE(e) {
- var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay
-
-
- setTimeout(function () {
- _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;
-
- if (e.target == this.DOM.scope) {
- // if( !this.state.hasFocus )
- // this.dropdown.hide.call(this)
- this.DOM.input.focus();
- return;
- } else if (e.target.classList.contains("tagify__tag__removeBtn")) {
- this.removeTag(e.target.parentNode);
- return;
- } else if (tagElm) {
- tagElmIdx = this.getNodeIndex(tagElm);
- this.trigger("click", {
- tag: tagElm,
- index: tagElmIdx,
- data: this.value[tagElmIdx],
- originalEvent: this.cloneEvent(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);
- return;
- }
-
- if (_s.mode == 'select') !this.state.dropdown.visible && this.dropdown.show.call(this);
- },
- onEditTagInput: function onEditTagInput(editableElm, e) {
- var tagElm = editableElm.closest('tag'),
- tagElmIdx = this.getNodeIndex(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
-
- if (value.length >= this.settings.dropdown.enabled) {
- this.state.editing.value = value;
- this.dropdown.show.call(this, value);
- }
-
- this.trigger("edit:input", {
- tag: tagElm,
- index: tagElmIdx,
- data: this.extend({}, this.value[tagElmIdx], {
- newValue: value
- }),
- originalEvent: this.cloneEvent(e)
- });
- },
- onEditTagBlur: function onEditTagBlur(editableElm) {
- if (!this.state.hasFocus) this.toggleFocusClass();
- 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()
-
-
- if (!currentValue) {
- this.removeTag(tagElm);
- return;
- }
-
- if (hasChanged) {
- this.settings.transformTag.call(this, tagData); // re-validate after tag transformation
-
- isValid = this.validateTag(tagData.value);
- } else {
- this.replaceTag(tagElm);
- return;
- }
-
- if (isValid !== undefined && isValid !== true) return;
- this.replaceTag(tagElm, tagData);
- },
- onEditTagkeydown: function onEditTagkeydown(e) {
- this.trigger("edit:keydown", {
- originalEvent: this.cloneEvent(e)
- });
-
- switch (e.key) {
- case 'Esc':
- case 'Escape':
- e.target.textContent = e.target.originalValue;
-
- case 'Enter':
- case 'Tab':
- e.preventDefault();
- e.target.blur();
- }
- },
- onDoubleClickScope: function onDoubleClickScope(e) {
- var tagElm = e.target.closest('tag'),
- _s = this.settings,
- isEditingTag,
- isReadyOnlyTag;
- if (!tagElm) return;
- isEditingTag = tagElm.classList.contains('tagify__tag--editable'), isReadyOnlyTag = tagElm.hasAttribute('readonly');
- if (_s.mode != 'select' && !_s.readonly && !isEditingTag && !isReadyOnlyTag) this.editTag(tagElm);
- this.toggleFocusClass(true);
- }
- }
- },
-
- /**
- * @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),
- _CB = this.events.callbacks,
- that = this,
- delayed_onEditTagBlur = function delayed_onEditTagBlur() {
- setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm);
- };
-
- if (!editableElm) {
- console.warn('Cannot find element in Tag template: ', '.tagify__tag-text');
- 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: this.value[tagIdx]
- });
- return this;
- },
-
- /**
- * 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;
-
- var editableElm = tagElm.querySelector('.tagify__tag-text'),
- clone = editableElm.cloneNode(true),
- tagElmIdx;
- 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.state.editing = {
- locked: true
- };
- setTimeout(function () {
- return delete _this7.state.editing.locked;
- }, 500); // update DOM nodes
-
- clone.removeAttribute('contenteditable');
- tagElm.classList.remove('tagify__tag--editable'); // guarantee to remove all events which were added by the "editTag" method
-
- editableElm.parentNode.replaceChild(clone, editableElm); // continue only if there was a reason for it
-
- if (tagData) {
- clone.innerHTML = tagData.value;
- clone.title = tagData.value; // update data
-
- tagElmIdx = this.getNodeIndex(tagElm);
- this.value[tagElmIdx] = tagData;
- this.update();
- this.trigger("edit:updated", {
- tag: tagElm,
- index: tagElmIdx,
- data: tagData
- });
- }
- },
-
- /** 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);
- });
- }
- },
-
- /**
- * 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
-
- this.input.autocomplete.suggest.call(this);
- this.input.validate.call(this);
- },
-
- /**
- * Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
- */
- 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
)
- 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
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");
-
- 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) {}
-
- v = v.replace(/\s/g, ' ') // replace NBSPs with spaces characters
- .replace(/^\s+/, ""); // trimLeft
-
- return v;
- },
-
- /**
- * suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
- * @param {String} s [description]
- */
- 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);
-
- 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();
- }
-
- this.input.autocomplete.suggest.call(this);
- this.dropdown.hide.call(this);
- return true;
- }
-
- return false;
- }
- }
- },
- 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;
-
- // 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];
- },
-
- /**
- * 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
-
- if (tagElm) {
- tagElm.classList.add('tagify--mark'); // setTimeout(() => { tagElm.classList.remove('tagify--mark') }, 100);
-
- return tagElm;
- }
-
- return false;
- },
-
- /**
- * 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;
- },
-
- /**
- * 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();
- });
- },
-
- /**
- * 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;
- },
-
- /**
- * 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()
- };
- });
- }; // no need to continue if "tagsItems" is an Array of Objects
-
-
- if (isCollection) {
- var _ref3;
-
- // iterate the collection items and check for values that can be splitted into multiple tags
- tagsItems = (_ref3 = []).concat.apply(_ref3, _toConsumableArray(tagsItems.map(function (item) {
- return mapStringToCollection(item.value).map(function (newItem) {
- return _objectSpread({}, item, {}, newItem);
- });
- })));
- return tagsItems;
- }
-
- if (typeof tagsItems == 'number') tagsItems = tagsItems.toString(); // if the value 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);
- } else if (isArray) {
- var _ref4;
-
- tagsItems = (_ref4 = []).concat.apply(_ref4, _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
-
-
- 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();
- });
-
- 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;
- }
-
- return tagsItems;
- },
-
- /**
- * 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) {
- var s2 = s1.split(mixTagsInterpolator[1]),
- preInterpolated = s2[0],
- tagData,
- tagElm;
-
- try {
- tagData = JSON.parse(preInterpolated);
- } catch (err) {
- tagData = _this9.normalizeTags(preInterpolated)[0]; //{value:preInterpolated}
- }
-
- 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; //+ "" // 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)
-
- _this9.value.push(tagData);
- } else if (s1) return mixTagsInterpolator[0] + s1;
-
- return s2.join('');
- }).join('');
- this.DOM.input.innerHTML = s;
- this.DOM.input.appendChild(document.createTextNode(''));
- this.DOM.input.appendChild(document.createTextNode(''));
- this.update();
- return s;
- },
-
- /**
- * 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); // #ba
- // 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;
- },
-
- /**
- * 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: ""
- },
- tagElm = this.createTagElem(tagData); // add the tag to the component's DOM
-
- this.appendTag(tagElm);
- this.value.push(tagData);
- this.update();
- this.editTag(tagElm);
- },
-
- /**
- * 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;
-
- var skipInvalid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.settings.skipInvalid;
- var tagElems = [],
- tagElm,
- _s = this.settings;
-
- if (!tagsItems || tagsItems.length == 0) {
- // is mode is "select" clean all tags
- if (_s.mode == 'select') this.removeAllTags(); // console.warn('[addTags]', 'no tags to add:', tagsItems)
-
- return tagElems;
- }
-
- tagsItems = this.normalizeTags(tagsItems); // converts Array/String/Object to an Array of Objects
- // if in edit-mode, do not continue but instead replace the tag's text
-
- if (this.state.editing.scope) {
- return this.replaceTag(this.state.editing.scope, tagsItems[0]);
- }
-
- if (_s.mode == 'mix') {
- _s.transformTag.call(this, tagsItems[0]);
-
- tagElm = this.createTagElem(tagsItems[0]); // insert the new tag to the END if "addTags" was called from outside
-
- 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)
-
-
- 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)
-
- this.DOM.input.appendChild(document.createTextNode(''));
- return tagElm;
- }
-
- 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
-
- tagData = Object.assign({}, tagData);
-
- _s.transformTag.call(_this10, tagData); ///////////////// ( validation )//////////////////////
-
-
- tagValidation = _this10.hasMaxTags() || _this10.validateTag(tagData.value);
-
- if (tagValidation !== true) {
- if (skipInvalid) return;
- tagElmParams["aria-invalid"] = true;
- tagElmParams["class"] = (tagData["class"] || '') + ' tagify--notAllowed';
- tagElmParams.title = tagValidation;
-
- _this10.markTagByValue(tagData.value);
- } /////////////////////////////////////////////////////
- // add accessibility attributes
-
-
- tagElmParams.role = "tag";
- if (tagData.readonly) tagElmParams["aria-readonly"] = true; // Create tag HTML element
-
- tagElm = _this10.createTagElem(_this10.extend({}, tagData, tagElmParams));
- tagElems.push(tagElm); // mode-select overrides
-
- if (_s.mode == 'select') {
- return _this10.selectTag(tagElm, tagData);
- } // add the tag to the component's DOM
-
-
- _this10.appendTag(tagElm);
-
- if (tagValidation === true) {
- // update state
- _this10.value.push(tagData);
-
- _this10.update();
-
- _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 (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false")
- setTimeout(function () {
- return _this10.removeTag(tagElm, true);
- }, 1000);
- }
-
- _this10.dropdown.position.call(_this10); // reposition the dropdown because the just-added tag might cause a new-line
-
- });
-
- if (tagsItems.length && clearInput) {
- this.input.set.call(this);
- }
-
- this.dropdown.refilter.call(this);
- return tagElems;
- },
-
- /**
- * 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);
- },
-
- /**
- * 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 ]+\<").replace(/(<.*?>)|\s+/g, function (m, $1) {
- return $1 ? $1 : ' ';
- }) // https://stackoverflow.com/a/44841484/104380
- : "";
- },
-
- /**
- * 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;
- },
-
- /**
- * 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);
- }
-
- if (tagElm.classList.contains('tagify--notAllowed')) silent = true;
-
- function removeNode() {
- if (!tagElm.parentNode) return;
- tagElm.parentNode.removeChild(tagElm);
-
- if (!silent) {
- tagData = that.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
-
- that.update(); // update the original input with the current value
-
- 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
- });
- }
-
- 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
-
- tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover)
-
- setTimeout(removeNode, tranDuration);
- }
-
- 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);
- },
-
- /**
- * 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;
- },
-
- /**
- * 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(""));
-
- return elm;
- },
- show: function show(value) {
- var _this12 = this;
-
- 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"
-
- 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;
-
- 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);
- }
- } 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.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 (!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
-
-
- setTimeout(this.dropdown.events.binding.bind(this));
- }
- },
- 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
-
- 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);
- },
-
- /**
- * 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: 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;
-
- 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";
- }
-
- 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
-
- 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;
-
- // 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.settings.dropdown.position != 'manual') {
- window[action]('resize', _CBR.position);
- window[action]('keydown', _CBR.onKeyDown);
- } // window[action]('mousedown', _CBR.onClick);
-
-
- 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.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;
-
- 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];
- }
-
- this.dropdown.highlightOption.call(this, selectedElm, true);
- break;
- }
-
- case 'Escape':
- case 'Esc':
- // IE11
- this.dropdown.hide.call(this);
- break;
-
- case 'ArrowRight':
- if (this.state.actions.ArrowLeft) return;
-
- case 'Tab':
- {
- e.preventDefault(); // in mix-mode, treat arrowRight like Enter key, so a tag will be created
-
- 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;
- }
- }
-
- case 'Enter':
- {
- e.preventDefault();
- this.dropdown.selectOption.call(this, activeListElm);
- break;
- }
-
- case 'Backspace':
- {
- if (this.settings.mode == 'mix' || this.state.editing.scope) return;
-
- var _value = this.input.value.trim();
-
- 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
-
- 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
-
- var listItemElm = e.target.closest(".tagify__dropdown__item");
- this.dropdown.selectOption.call(this, listItemElm);
- }
- }
- },
-
- /**
- * 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: 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();
-
- 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("[class$='--active']").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);
- 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
- }
- },
-
- /**
- * Create a tag from the currently active suggestion option
- * @param {Object} elm DOM node to select
- */
- selectOption: function selectOption(elm) {
- var _this13 = this;
-
- if (!elm) return; // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run
-
- 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
-
- setTimeout(function () {
- _this13.DOM.input.focus();
-
- _this13.toggleFocusClass(true);
- });
-
- if (hideDropdown) {
- this.dropdown.hide.call(this); // setTimeout(() => this.events.callbacks.onFocusBlur.call(this, {type:"blur"}), 60)
- }
- },
-
- /**
- * 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
- */
- filterListItems: function filterListItems(value) {
- var _this14 = this;
-
- 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;
-
- 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
- }
-
- for (; i < whitelist.length; i++) {
- whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : {
- value: whitelist[i]
- }; //normalize value as an Object
-
- 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 (valueIsInWhitelist && !isDuplicate && suggestionsCount--) list.push(whitelistItem);
- if (suggestionsCount == 0) break;
- }
-
- 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(""));
- }
- }
-};
-return 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\n \n ')},tag:function(t,e){return"\n \n \n ").concat(t,"\n
\n ")},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,"'");return"').concat(i,"
")}},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=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),void this.trigger("click",{tag:i,index:e,data:this.value[e],originalEvent:this.cloneEvent(t)})):void(t.target==this.DOM.input&&500=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.editTag(s),this.toggleFocusClass(!0))}}},editTag:function(t){var e=this,i=0=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 a=this,e=this.settings,o=e.mixTagsInterpolator,r=e.duplicates,l=e.transformTag,d=e.enforceWhitelist;return t=t.split(o[0]).map(function(t){var e,i,s=t.split(o[1]),n=s[0];try{e=JSON.parse(n)}catch(t){e=a.normalizeTags(n)[0]}if(!(1[\r\n ]+\<").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=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(''))},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