New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@yaireo/tagify

Package Overview
Dependencies
Maintainers
1
Versions
270
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@yaireo/tagify - npm Package Compare versions

Comparing version 2.1.9 to 2.3.0

1595

dist/jQuery.tagify.js
"use strict";
/**
* Tagify (v 2.1.8)- tags input component
* Tagify (v 2.2.10)- tags input component
* By Yair Even-Or (2016)

@@ -9,964 +9,931 @@ * Don't sell this code. (c)

*/
;(function ($) {
// just a jQuery wrapper for the vanilla version of this component
$.fn.tagify = function () {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
;
return this.each(function () {
var $input = $(this),
tagify;
(function ($) {
// just a jQuery wrapper for the vanilla version of this component
$.fn.tagify = function () {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return this.each(function () {
var $input = $(this),
tagify;
if ($input.data("tagify")) // don't continue if already "tagified"
return this;
settings.isJQueryPlugin = true;
tagify = new Tagify($input[0], settings);
$input.data("tagify", tagify);
});
};
if ($input.data("tagify")) // don't continue if already "tagified"
return this;
function Tagify(input, settings) {
// protection
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
settings.isJQueryPlugin = true;
tagify = new Tagify($input[0], settings);
$input.data("tagify", tagify);
});
};
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
function Tagify(input, settings) {
// protection
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
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
if (input.pattern) try {
this.settings.pattern = new RegExp(input.pattern);
} catch (e) {} // Convert the "delimiters" setting into a REGEX object
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
if (input.pattern) try {
this.settings.pattern = new RegExp(input.pattern);
} catch (e) {}
// Convert the "delimiters" setting into a REGEX object
if (this.settings && this.settings.delimiters) {
try {
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g");
} catch (e) {}
}
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.loadOriginalValues();
this.events.customBinding.call(this);
this.events.binding.call(this);
if (this.settings && this.settings.delimiters) {
try {
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g");
} catch (e) {}
}
Tagify.prototype = {
isIE: window.document.documentMode,
this.value = []; // tags' data
// events' callbacks references will be stores here, so events could be unbinded
TEXTS: {
empty: "empty",
exceed: "number of tags exceeded",
pattern: "pattern mismatch",
duplicate: "already exists",
notAllowed: "not allowed"
},
this.listeners = {};
this.DOM = {}; // Store all relevant DOM elements in an Object
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
autoComplete: true, // Flag - tries to autocomplete the input's value while typing
mapValueToProp: "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value"
dropdown: {
classname: '',
enabled: 2, // minimum input characters needs to be typed for the dropdown to show
maxItems: 10,
itemTemplate: ''
}
},
this.extend(this, new this.EventDispatcher(this));
this.build(input);
this.loadOriginalValues();
this.events.customBinding.call(this);
this.events.binding.call(this);
}
customEventsList: ['add', 'remove', 'invalid'],
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
autoComplete: true,
// Flag - tries to autocomplete the input's value while typing
dropdown: {
classname: '',
enabled: 2,
// minimum input characters needs to be typed for the dropdown to show
maxItems: 10,
itemTemplate: ''
}
},
customEventsList: ['add', 'remove', 'invalid'],
/**
* 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");
/**
* 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;
},
// 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;
},
return node.body.firstElementChild;
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build: function build(input) {
var that = this,
DOM = this.DOM,
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>";
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.settings.whitelist.length) {
this.dropdown.init.call(this);
}
// 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;
},
input.autofocus && DOM.input.focus();
},
/**
* Reverts back any changes made by this component
*/
destroy: function destroy() {
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build: function build(input) {
var that = this,
DOM = this.DOM,
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>";
/**
* Merge two objects into a new one
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
*/
extend: function extend(o, o1, o2) {
if (!(o instanceof Object)) o = {};
copy(o, o1);
if (o2) copy(o, o2);
DOM.originalInput = input;
DOM.scope = this.parseHTML(template);
DOM.input = DOM.scope.querySelector('[contenteditable]');
input.parentNode.insertBefore(DOM.scope, input);
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';
}
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) {
this.dropdown.init.call(this);
}
;
input.autofocus && DOM.input.focus();
},
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]);
} else a[key] = b[key];
}
}
}
return o;
},
/**
* Reverts back any changes made by this component
*/
destroy: function destroy() {
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
/**
* A constructor for exposing events to the outside
*/
EventDispatcher: function EventDispatcher(instance) {
// Create a DOM EventTarget object
var target = document.createTextNode(''); // Pass EventTarget interface calls to DOM EventTarget object
this.off = function (name, cb) {
if (cb) target.removeEventListener.call(target, name, cb);
return this;
};
/**
* Merge two objects into a new one
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
*/
extend: function extend(o, o1, o2) {
if (!(o instanceof Object)) o = {};
this.on = function (name, cb) {
if (cb) target.addEventListener.call(target, name, cb);
return this;
};
copy(o, o1);
if (o2) copy(o, o2);
this.trigger = function (eventName, data) {
var e;
if (!eventName) return;
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';
};
if (instance.settings.isJQueryPlugin) {
$(instance.DOM.originalInput).triggerHandler(eventName, [data]);
} else {
try {
e = new CustomEvent(eventName, {
"detail": data
});
} catch (err) {
console.warn(err);
}
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]);
} else a[key] = b[key];
}
}
}
target.dispatchEvent(e);
}
};
},
return o;
},
/**
* DOM events listeners binding
*/
events: {
// bind custom events which were passed in the settings
customBinding: function customBinding() {
var _this2 = this;
this.customEventsList.forEach(function (name) {
_this2.on(name, _this2.settings.callbacks[name]);
});
},
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
/**
* A constructor for exposing events to the outside
*/
EventDispatcher: function EventDispatcher(instance) {
// Create a DOM EventTarget object
var target = document.createTextNode('');
// Pass EventTarget interface calls to DOM EventTarget object
this.off = function (name, cb) {
if (cb) target.removeEventListener.call(target, name, cb);
return this;
};
this.on = function (name, cb) {
if (cb) target.addEventListener.call(target, name, cb);
return this;
};
this.trigger = function (eventName, data) {
var e;
if (!eventName) return;
if (instance.settings.isJQueryPlugin) {
$(instance.DOM.originalInput).triggerHandler(eventName, [data]);
} else {
try {
e = new CustomEvent(eventName, { "detail": data });
} catch (err) {
console.warn(err);
}
target.dispatchEvent(e);
}
};
var _CB = this.events.callbacks,
// setup callback references so events could be removed later
_CBR = this.listeners.main = this.listeners.main || {
paste: ['input', _CB.onPaste.bind(this)],
focus: ['input', _CB.onFocusBlur.bind(this)],
blur: ['input', _CB.onFocusBlur.bind(this)],
keydown: ['input', _CB.onKeydown.bind(this)],
click: ['scope', _CB.onClickScope.bind(this)]
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
for (var eventName in _CBR) {
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
/**
* DOM events listeners binding
*/
events: {
// bind custom events which were passed in the settings
customBinding: function customBinding() {
var _this2 = this;
if (bindUnbind) {
// 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) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
}
},
this.customEventsList.forEach(function (name) {
_this2.on(name, _this2.settings.callbacks[name]);
});
},
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
/**
* DOM events callbacks
*/
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
var _CB = this.events.callbacks,
// setup callback references so events could be removed later
_CBR = this.listeners.main = this.listeners.main || {
paste: ['input', _CB.onPaste.bind(this)],
focus: ['input', _CB.onFocusBlur.bind(this)],
blur: ['input', _CB.onFocusBlur.bind(this)],
keydown: ['input', _CB.onKeydown.bind(this)],
click: ['scope', _CB.onClickScope.bind(this)]
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
for (var eventName in _CBR) {
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
if (bindUnbind) {
// 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) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
}
},
/**
* DOM events callbacks
*/
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
if (this.settings.dropdown.enabled === 0) {
this.dropdown.show.call(this);
}
} else if (e.type == "blur" && s) {
this.settings.addTagOnBlur && this.addTags(s, true).length;
} else {
// e.target.classList.add('placeholder');
this.DOM.input.removeAttribute('style');
this.dropdown.hide.call(this);
}
},
onKeydown: function onKeydown(e) {
var s = e.target.textContent,
lastTag;
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) {
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)');
lastTag = lastTag[lastTag.length - 1];
this.removeTag(lastTag);
} else if (e.key == 'Escape' || e.key == 'Esc') {
this.input.set.call(this);
e.target.blur();
} else if (e.key == 'Enter') {
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
this.addTags(this.input.value, true);
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this);
},
onInput: function onInput(e) {
var value = this.input.normalize.call(this),
showSuggestions = value.length >= this.settings.dropdown.enabled;
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
// save the value on the input's State object
this.input.set.call(this, value, false);
if (value.search(this.settings.delimiters) != -1) {
if (this.addTags(value).length) {
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);
}
},
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);
});
},
onPaste: function onPaste(e) {},
onClickScope: function onClickScope(e) {
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") {
this.removeTag(e.target.parentNode);
}
}
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
if (this.settings.dropdown.enabled === 0) {
this.dropdown.show.call(this);
}
} else if (e.type == "blur" && s) {
this.settings.addTagOnBlur && this.addTags(s, true).length;
} else {
// e.target.classList.add('placeholder');
this.DOM.input.removeAttribute('style');
this.dropdown.hide.call(this);
}
},
onKeydown: function onKeydown(e) {
var s = e.target.textContent,
lastTag;
/**
* If the original input had an values, add them as tags
*/
loadOriginalValues: function loadOriginalValues() {
var value = this.DOM.originalInput.value,
values;
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) {
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)');
lastTag = lastTag[lastTag.length - 1];
this.removeTag(lastTag);
} else if (e.key == 'Escape' || e.key == 'Esc') {
this.input.set.call(this);
e.target.blur();
} else if (e.key == 'Enter') {
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
// if the original input already had any value (tags)
if (!value) return;
this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
this.addTags(this.input.value, true);
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this);
},
onInput: function onInput(e) {
var value = this.input.normalize.call(this),
showSuggestions = value.length >= this.settings.dropdown.enabled;
if (!value) {
this.input.set.call(this, '');
return;
}
/**
* 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;
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead
// save the value on the input's State object
this.input.value = s;
this.input.set.call(this, value, false);
if (updateDOM) this.DOM.input.innerHTML = s;
if (!s) this.dropdown.hide.call(this);
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
this.input.validate.call(this);
},
// https://stackoverflow.com/a/3866442/104380
setRangeAtEnd: function setRangeAtEnd() {
var range, selection;
if (!document.createRange) return;
range = document.createRange();
range.selectNodeContents(this.DOM.input);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
/**
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
*/
validate: function validate() {
var isValid = !this.input.value || this.validateTag.call(this, this.input.value);
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() {
var clone = this.DOM.input.cloneNode(true),
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters
while (clone.firstElementChild) {
v += clone.firstElementChild.textContent;
clone.removeChild(clone.firstElementChild);
}
return v.replace(/^\s+/, ""); // trimLeft
},
/**
* suggest the rest of the input's value
* @param {String} s [description]
*/
autocomplete: {
suggest: function suggest(s) {
if (!this.DOM.input.value) return; // do not suggest anything for empty input
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest");
},
set: function set(s) {
var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null);
if (suggestion) {
this.input.set.call(this, suggestion);
this.input.autocomplete.suggest.call(this, '');
this.dropdown.hide.call(this);
this.input.setRangeAtEnd.call(this);
}
// if( suggestion && this.addTags(this.input.value + suggestion).length ){
// this.input.set.call(this);
// this.dropdown.hide.call(this);
// }
}
if (value.search(this.settings.delimiters) != -1) {
if (this.addTags(value).length) {
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);
}
},
onInputIE: function onInputIE(e) {
var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay
getNodeIndex: function getNodeIndex(node) {
var index = 0;
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}return index;
},
/**
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {int} [Position index of the tag. -1 is returned if tag is not found.]
*/
isTagDuplicate: function isTagDuplicate(s) {
return this.value.findIndex(function (item) {
return s.toLowerCase() === item.value.toLowerCase();
});
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
setTimeout(function () {
_this.events.callbacks.onInput.call(_this, e);
});
},
onPaste: function onPaste(e) {},
onClickScope: function onClickScope(e) {
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") {
this.removeTag(e.target.parentNode);
}
}
}
},
/**
* If the original input had an values, add them as tags
*/
loadOriginalValues: function loadOriginalValues() {
var value = this.DOM.originalInput.value; // if the original input already had any value (tags)
/**
* 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) {
var tagsElms, tagsElmsLen, tagIdx;
if (!value) return;
if (!tagElm) {
tagIdx = this.isTagDuplicate.call(this, value);
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx];
}
try {
value = JSON.parse(value);
} catch (err) {}
// check AGAIN if "tagElm" is defined
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 100);
return tagElm;
}
this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
},
return false;
},
/**
* 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;
this.input.value = s;
if (updateDOM) this.DOM.input.innerHTML = s;
if (!s) this.dropdown.hide.call(this);
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
this.input.validate.call(this);
},
// https://stackoverflow.com/a/3866442/104380
setRangeAtEnd: function setRangeAtEnd() {
var range, selection;
if (!document.createRange) return;
range = document.createRange();
range.selectNodeContents(this.DOM.input);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
/**
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
*/
validate: function validate() {
var isValid = !this.input.value || this.validateTag.call(this, this.input.value);
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() {
var clone = this.DOM.input.cloneNode(true),
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted: function isTagBlacklisted(v) {
v = v.split(' ');
return this.settings.blacklist.filter(function (x) {
return v.indexOf(x) != -1;
}).length;
},
while (clone.firstElementChild) {
v += clone.firstElementChild.textContent;
clone.removeChild(clone.firstElementChild);
}
return v.replace(/^\s+/, ""); // trimLeft
},
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagWhitelisted: function isTagWhitelisted(v) {
return this.settings.whitelist.some(function (item) {
var value = item.value ? item.value : item;
if (value.toLowerCase() === v.toLowerCase()) return true;
});
/**
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
* @param {String} s [description]
*/
autocomplete: {
suggest: function suggest(s) {
if (!s || !this.input.value) this.DOM.input.removeAttribute("data-suggest");else this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));
},
set: function set(s) {
var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null);
if (suggestion) {
this.input.set.call(this, suggestion);
this.input.autocomplete.suggest.call(this, '');
this.dropdown.hide.call(this);
this.input.setRangeAtEnd.call(this);
} // if( suggestion && this.addTags(this.input.value + suggestion).length ){
// this.input.set.call(this);
// this.dropdown.hide.call(this);
// }
/**
* 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(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
result = true;
}
}
},
getNodeIndex: function getNodeIndex(node) {
var index = 0;
// check for empty value
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed;
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}
// check if pattern should be used and if so, use it to test the value
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern;
return index;
},
// if duplicates are not allowed and there is a duplicate
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed;
/**
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {int} [Position index of the tag. -1 is returned if tag is not found.]
*/
isTagDuplicate: function isTagDuplicate(s) {
return this.value.findIndex(function (item) {
return s.trim().toLowerCase() === item.value.toLowerCase();
}); // return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
},
getTagIndexByValue: function getTagIndexByValue(value) {
var result = [];
this.DOM.scope.querySelectorAll('tag').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.DOM.scope.querySelectorAll('tag')[tagIdx];
},
return result;
},
/**
* 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) {
var tagsElms, tagsElmsLen;
tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 100);
return tagElm;
}
/**
* 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 _this3 = this;
return false;
},
var whitelistWithProps = this.settings.whitelist[0] instanceof Object,
isComplex = tagsItems instanceof Array && "value" in tagsItems[0],
// checks if the value is a "complex" which means an Array of Objects, each object is a tag
temp = [];
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted: function isTagBlacklisted(v) {
v = v.split(' ');
return this.settings.blacklist.filter(function (x) {
return v.indexOf(x) != -1;
}).length;
},
// no need to continue if "tagsItems" is an Array of Objects
if (isComplex) return tagsItems;
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagWhitelisted: function isTagWhitelisted(v) {
return this.settings.whitelist.some(function (item) {
var value = item.value ? item.value : item;
if (value.toLowerCase() === v.toLowerCase()) return true;
});
},
// if the value is a "simple" String, ex: "aaa, bbb, ccc"
if (!isComplex) {
if (!tagsItems.trim()) return [];
/**
* 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(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
result = true; // check for empty value
// go over each tag and add it (if there were multiple ones)
tagsItems = tagsItems.split(this.settings.delimiters).filter(function (n) {
return n;
}).map(function (v) {
return { value: v.trim() };
});
}
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; // check if pattern should be used and if so, use it to test the value
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed;
return result;
},
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if (!isComplex && whitelistWithProps) {
tagsItems.forEach(function (tag) {
var matchObj = _this3.settings.whitelist.filter(function (WL_item) {
return WL_item.value.toLowerCase() == tag.value.toLowerCase();
});
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value
else temp.push(tag);
});
/**
* 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 _this3 = this;
tagsItems = temp;
}
var whitelistWithProps = this.settings.whitelist[0] instanceof Object,
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0],
// checks if the value is a "complex" which means an Array of Objects, each object is a tag
temp = []; // no need to continue if "tagsItems" is an Array of Objects
return tagsItems;
},
if (isComplex) return tagsItems; // 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)
/**
* 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]
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags]
* @return {Array} Array of DOM elements (tags)
*/
addTags: function addTags(tagsItems, clearInput) {
var _this4 = this;
return tagsItems.split(this.settings.delimiters).filter(function (n) {
return n;
}).map(function (v) {
return {
value: v.trim()
};
});
}
var tagElems = [];
if (tagsItems instanceof Array) return tagsItems.map(function (v) {
return {
value: v.trim()
};
}); // search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
this.DOM.input.removeAttribute('style');
if (whitelistWithProps) {
tagsItems.forEach(function (tag) {
var matchObj = _this3.settings.whitelist.filter(function (WL_item) {
return WL_item.value.toLowerCase() == tag.value.toLowerCase();
});
tagsItems = this.normalizeTags.call(this, tagsItems);
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value
else temp.push(tag);
});
return temp;
}
},
tagsItems.forEach(function (tagData) {
var tagValidation, 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]
* @return {Array} Array of DOM elements (tags)
*/
addTags: function addTags(tagsItems, clearInput) {
var _this4 = this;
if (typeof _this4.settings.transformTag === 'function') {
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value;
}
var tagElems = [];
this.DOM.input.removeAttribute('style');
tagsItems = this.normalizeTags.call(this, tagsItems);
tagsItems.forEach(function (tagData) {
var tagValidation, tagElm;
tagValidation = _this4.validateTag.call(_this4, tagData.value);
if (typeof _this4.settings.transformTag === 'function') {
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value;
}
if (tagValidation !== true) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
tagData.title = tagValidation;
_this4.markTagByValue.call(_this4, tagData.value);
_this4.trigger("invalid", { value: tagData.value, index: _this4.value.length, message: tagValidation });
}
tagValidation = _this4.validateTag.call(_this4, tagData.value);
// Create tag HTML element
tagElm = _this4.createTagElem(tagData);
tagElems.push(tagElm);
if (tagValidation !== true) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
tagData.title = tagValidation;
// add the tag to the component's DOM
appendTag.call(_this4, tagElm);
_this4.markTagByValue(tagData.value);
if (tagValidation === true) {
// update state
_this4.value.push(tagData);
_this4.update();
_this4.trigger('add', _this4.extend({}, { index: _this4.value.length, tag: tagElm }, tagData));
} else if (!_this4.settings.keepInvalidTags) {
// remove invalid tags (if "keepInvalidTags" is set to "false")
setTimeout(function () {
_this4.removeTag(tagElm, true);
}, 1000);
}
});
_this4.trigger("invalid", {
value: tagData.value,
index: _this4.value.length,
message: tagValidation
});
} // Create tag HTML element
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
function appendTag(tagElm) {
var insertBeforeNode = this.DOM.scope.lastElementChild;
tagElm = _this4.createTagElem(tagData);
tagElems.push(tagElm); // add the tag to the component's DOM
if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm);
}
appendTag.call(_this4, tagElm);
return tagElems;
},
if (tagValidation === true) {
// update state
_this4.value.push(tagData);
_this4.update();
/**
* 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 = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>";
_this4.trigger('add', _this4.extend({}, {
index: _this4.value.length,
tag: tagElm
}, tagData));
} else if (!_this4.settings.keepInvalidTags) {
// remove invalid tags (if "keepInvalidTags" is set to "false")
setTimeout(function () {
_this4.removeTag(tagElm, true);
}, 1000);
}
});
if (typeof this.settings.tagTemplate === "function") {
try {
template = this.settings.tagTemplate(v, tagData);
} catch (err) {}
}
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
// for a certain Tag element, add attributes.
function addTagAttrs(tagElm, tagData) {
var i,
keys = Object.keys(tagData);
for (i = keys.length; i--;) {
var propName = keys[i];
if (!tagData.hasOwnProperty(propName)) return;
tagElm.setAttribute(propName, tagData[propName]);
}
}
tagElm = this.parseHTML(template);
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);
}
// add any attribuets, if exists
addTagAttrs(tagElm, tagData);
return tagElems;
},
return tagElm;
},
/**
* 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 = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>";
if (typeof this.settings.tagTemplate === "function") {
try {
template = this.settings.tagTemplate(v, tagData);
} catch (err) {}
} // for a certain Tag element, add attributes.
/**
* Removes a tag
* @param {Object} tagElm [DOM element]
* @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) {
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250;
if (!tagElm) return;
function addTagAttrs(tagElm, tagData) {
var i,
keys = Object.keys(tagData);
var tagData,
tagIdx = this.getNodeIndex(tagElm);
for (i = keys.length; i--;) {
var propName = keys[i];
if (!tagData.hasOwnProperty(propName)) return;
tagElm.setAttribute(propName, tagData[propName]);
}
}
if (!tagElm) return;
tagElm = this.parseHTML(template); // add any attribuets, if exists
if (tranDuration && tranDuration > 10) animation();else removeNode();
addTagAttrs(tagElm, tagData);
return tagElm;
},
if (!silent) {
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
this.update(); // update the original input with the current value
this.trigger('remove', this.extend({}, { index: tagIdx, tag: tagElm }, tagData));
}
/**
* Removes a tag
* @param {Object|String} tagElm [DOM element or a String value]
* @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) {
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250;
if (!tagElm) return;
if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm);
var tagData,
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable)
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');
if (tranDuration && tranDuration > 10) animation();else removeNode();
// manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(removeNode, 400);
}
if (!silent) {
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
function removeNode() {
tagElm.parentNode.removeChild(tagElm);
}
},
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
},
this.update(); // update the original input with the current value
this.trigger('remove', this.extend({}, {
index: tagIdx,
tag: tagElm
}, tagData));
}
/**
* update the origianl (hidden) input field's value
* see - https://stackoverflow.com/q/50957841/104380
*/
update: function update() {
var _this5 = this;
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
var tagsAsString = this.value.map(function (v) {
return v[_this5.settings.mapValueToProp || "value"] || v.value;
});
this.DOM.originalInput.value = JSON.stringify(tagsAsString);
},
tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(removeNode, 400);
}
/**
* Dropdown controller
* @type {Object}
*/
dropdown: {
init: function init() {
this.DOM.dropdown = this.dropdown.build.call(this);
},
build: function build() {
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(),
template = "<div class=\"" + className + "\"></div>";
return this.parseHTML(template);
},
show: function show(value) {
var listItems, listHTML;
function removeNode() {
tagElm.parentNode.removeChild(tagElm);
}
},
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
},
if (!this.settings.whitelist.length) return;
/**
* update the origianl (hidden) input field's value
* see - https://stackoverflow.com/q/50957841/104380
*/
update: function update() {
this.DOM.originalInput.value = JSON.stringify(this.value);
},
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0);
/**
* Dropdown controller
* @type {Object}
*/
dropdown: {
init: function init() {
this.DOM.dropdown = this.dropdown.build.call(this);
},
build: function build() {
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(),
template = "<div class=\"" + className + "\"></div>";
return this.parseHTML(template);
},
show: function show(value) {
var listItems, listHTML;
if (!this.settings.whitelist.length) return; // if no value was supplied, show all the "whitelist" items in the dropdown
// @type [Array] listItems
listHTML = this.dropdown.createListHTML.call(this, listItems);
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0);
listHTML = this.dropdown.createListHTML.call(this, listItems); // set the first item from the suggestions list as the autocomplete value
// set the first item from the suggestions list as the autocomplete value
if (this.settings.autoComplete) {
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : '');
}
if (this.settings.autoComplete) {
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : '');
} // if( !listHTML || listItems.length < 2 ){
// this.dropdown.hide.call(this);
// return;
// }
if (!listHTML || listItems.length < 2) {
this.dropdown.hide.call(this);
return;
}
this.DOM.dropdown.innerHTML = listHTML;
this.dropdown.position.call(this);
this.DOM.dropdown.innerHTML = listHTML;
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 the dropdown has yet to be appended to the document,
// append the dropdown to the body element & handle events
if (!this.DOM.dropdown.parentNode != document.body) {
document.body.appendChild(this.DOM.dropdown);
this.events.binding.call(this, false); // unbind the main events
this.dropdown.events.binding.call(this);
}
},
hide: function hide() {
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return;
if (!this.DOM.dropdown.parentNode != document.body) {
document.body.appendChild(this.DOM.dropdown);
this.events.binding.call(this, false); // unbind the main events
document.body.removeChild(this.DOM.dropdown);
window.removeEventListener('resize', this.dropdown.position);
this.dropdown.events.binding.call(this);
}
},
hide: function hide() {
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return;
document.body.removeChild(this.DOM.dropdown);
window.removeEventListener('resize', this.dropdown.position);
this.dropdown.events.binding.call(this, false); // unbind all events
this.dropdown.events.binding.call(this, false); // unbind all events
this.events.binding.call(this); // re-bind main events
},
position: function position() {
var rect = this.DOM.scope.getBoundingClientRect();
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \
this.events.binding.call(this); // re-bind main events
},
position: function position() {
var rect = this.DOM.scope.getBoundingClientRect();
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \
width: " + rect.width + "px";
},
},
/**
* @type {Object}
*/
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]
* @return {[type]} [description]
*/
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
/**
* @type {Object}
*/
events: {
// references to the ".bind()" methods must be saved so they could be unbinded later
var _CBR = this.listeners.dropdown = this.listeners.dropdown || {
position: this.dropdown.position.bind(this),
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this),
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this),
onClick: this.dropdown.events.callbacks.onClick.bind(this)
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
/**
* 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]
* @return {[type]} [description]
*/
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
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]('click', _CBR.onClick);
},
callbacks: {
onKeyDown: function onKeyDown(e) {
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],
newValue = "";
// references to the ".bind()" methods must be saved so they could be unbinded later
var _CBR = this.listeners.dropdown = this.listeners.dropdown || {
position: this.dropdown.position.bind(this),
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this),
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this),
onClick: this.dropdown.events.callbacks.onClick.bind(this)
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Down': // >IE11
window[action]('resize', _CBR.position);
window[action]('keydown', _CBR.onKeyDown);
window[action]('mousedown', _CBR.onClick);
case 'Up':
// >IE11
e.preventDefault();
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver);
// this.DOM.dropdown[action]('click', _CBR.onClick);
},
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm, true);
break;
case 'Escape':
case 'Esc':
// IE11
this.dropdown.hide.call(this);
break;
callbacks: {
onKeyDown: function onKeyDown(e) {
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],
newValue = "";
case 'Enter':
e.preventDefault();
newValue = selectedElm ? selectedElm.textContent : this.input.value;
this.addTags(newValue, true);
this.dropdown.hide.call(this);
break;
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Down': // >IE11
case 'Up':
// >IE11
e.preventDefault();
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"];
case 'ArrowRight':
case 'Tab':
e.preventDefault();
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null);
return false;
}
},
onMouseOver: function onMouseOver(e) {
// event delegation check
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target);
},
onClick: function onClick(e) {
var _this5 = this;
// if no element was found, loop
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm, true);
break;
case 'Escape':
case 'Esc':
// IE11
this.dropdown.hide.call(this);
break;
case 'Enter':
e.preventDefault();
newValue = selectedElm ? selectedElm.textContent : this.input.value;
this.addTags(newValue, true);
this.dropdown.hide.call(this);
break;
case 'ArrowRight':
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null);
break;
}
},
onMouseOver: function onMouseOver(e) {
// event delegation check
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target);
},
onClick: function onClick(e) {
var _this6 = this;
var onClickOutside = function onClickOutside() {
return _this6.dropdown.hide.call(_this6);
},
listItemElm;
if (e.button != 0) return; // allow only mouse left-clicks
if (e.target == document.documentElement) return onClickOutside();
listItemElm = [e.target, e.target.parentNode].filter(function (a) {
return a.className.includes("tagify__dropdown__item");
})[0];
if (listItemElm) {
this.input.set.call(this);
this.addTags(listItemElm.textContent);
}
// clicked outside the dropdown, so just close it
else onClickOutside();
}
}
var onClickOutside = function onClickOutside() {
return _this5.dropdown.hide.call(_this5);
},
listItemElm;
highlightOption: function highlightOption(elm, adjustScroll) {
if (!elm) return;
var className = "tagify__dropdown__item--active";
if (e.button != 0) return; // allow only mouse left-clicks
// for IE support, which doesn't allow "forEach" on "NodeList" Objects
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) {
return activeElm.classList.remove(className);
});
if (e.target == document.documentElement) return onClickOutside();
listItemElm = [e.target, e.target.parentNode].filter(function (a) {
return a.className.includes("tagify__dropdown__item");
})[0];
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
if (listItemElm) {
this.input.set.call(this);
this.addTags(listItemElm.textContent);
} // clicked outside the dropdown, so just close it
else onClickOutside();
}
}
},
highlightOption: function highlightOption(elm, adjustScroll) {
if (!elm) return;
var className = "tagify__dropdown__item--active"; // for IE support, which doesn't allow "forEach" on "NodeList" Objects
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight;
},
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) {
return activeElm.classList.remove(className);
}); // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight;
},
/**
* returns an HTML string of the suggestions' list items
* @return {[type]} [description]
*/
filterListItems: function filterListItems(value) {
if (!value) return "";
/**
* returns an HTML string of the suggestions' list items
* @return {[type]} [description]
*/
filterListItems: function filterListItems(value) {
if (!value) return "";
var list = [],
whitelist = this.settings.whitelist,
suggestionsCount = this.settings.dropdown.maxItems || Infinity,
whitelistItem,
valueIsInWhitelist,
i = 0;
var list = [],
whitelist = this.settings.whitelist,
suggestionsCount = this.settings.dropdown.maxItems || Infinity,
whitelistItem,
valueIsInWhitelist,
i = 0;
for (; i < whitelist.length; i++) {
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : {
value: whitelist[i]
}, //normalize value as an Object
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">="
// match for the value within each "whitelist" item
for (; i < whitelist.length; i++) {
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value: whitelist[i] }, //normalize value as an Object
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">="
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem);
if (suggestionsCount == 0) break;
}
// match for the value within each "whitelist" item
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem);
if (suggestionsCount == 0) break;
}
return list;
},
return list;
},
/**
* Creates the dropdown items' HTML
* @param {Array} list [Array of Objects]
* @return {String}
*/
createListHTML: function createListHTML(list) {
var getItem = this.settings.dropdown.itemTemplate || function (item) {
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>";
}; // for a certain Tag element, add attributes.
/**
* Creates the dropdown items' HTML
* @param {Array} list [Array of Objects]
* @return {String}
*/
createListHTML: function createListHTML(list) {
var getItem = this.settings.dropdown.itemTemplate || function (item) {
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>";
};
function getAttributesString(item) {
var i,
keys = Object.keys(item),
s = "";
// for a certain Tag element, add attributes.
function getAttributesString(item) {
var i,
keys = Object.keys(item),
s = "";
for (i = keys.length; i--;) {
var propName = keys[i];
if (propName != 'class' && !item.hasOwnProperty(propName)) return;
s += " " + propName + (item[propName] ? "=" + item[propName] : "");
}
return s;
}
for (i = keys.length; i--;) {
var propName = keys[i];
if (propName != 'class' && !item.hasOwnProperty(propName)) return;
s += " " + propName + (item[propName] ? "=" + item[propName] : "");
}
return list.map(getItem).join("");
}
return s;
}
};
return list.map(getItem).join("");
}
}
};
})(jQuery);

@@ -1,1 +0,1 @@

"use strict";!function(a){function n(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}a.fn.tagify=function(){var i=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};return this.each(function(){var t,e=a(this);if(e.data("tagify"))return this;i.isJQueryPlugin=!0,t=new n(e[0],i),e.data("tagify",t)})},n.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,autoComplete:!0,mapValueToProp:"",dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],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},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';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.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(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 s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)a(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&a(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;t&&this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){this.DOM.input.value&&(t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest"))},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.toLowerCase()===t.value.toLowerCase()})},markTagByValue:function(t,e){var i;return e||(i=this.isTagDuplicate.call(this,t),e=this.DOM.scope.querySelectorAll("tag")[i]),!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&"value"in t[0],s=[];if(n)return t;if(!n){if(!t.trim())return[];t=t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}})}return!n&&e&&(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),t=s),t},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue.call(n,t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){var n,s=this.getNodeIndex(t);t&&(i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n))))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){var e=this,t=this.value.map(function(t){return t[e.settings.mapValueToProp||"value"]||t.value});this.DOM.originalInput.value=JSON.stringify(t)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),!i||e.length<2?this.dropdown.hide.call(this):(this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this))))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":this.input.autocomplete.set.call(this,e?e.textContent:null)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}}}(jQuery);
"use strict";!function(a){function n(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}a.fn.tagify=function(){var i=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};return this.each(function(){var t,e=a(this);if(e.data("tagify"))return this;i.isJQueryPlugin=!0,t=new n(e[0],i),e.data("tagify",t)})},n.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,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],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},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';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.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(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 s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)a(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&a(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;if(t){try{t=JSON.parse(t)}catch(t){}this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})}},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){t&&this.input.value?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.trim().toLowerCase()===t.value.toLowerCase()})},getTagIndexByValue:function(i){var n=[];return this.DOM.scope.querySelectorAll("tag").forEach(function(t,e){t.textContent.trim().toLowerCase()==i.toLowerCase()&&n.push(e)}),n},getTagElmByValue:function(t){var e=this.getTagIndexByValue(t)[0];return this.DOM.scope.querySelectorAll("tag")[e]},markTagByValue:function(t,e){return!!(e=e||this.getTagElmByValue(t))&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&t[0]instanceof Object&&"value"in t[0],s=[];return n?t:"string"==typeof t?t.trim()?t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}}):[]:t instanceof Array?t.map(function(t){return{value:t.trim()}}):e?(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),s):void 0},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue(t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){"string"==typeof t&&(t=this.getTagElmByValue(t));var n,s=this.getTagIndexByValue(t.textContent);i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n)))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=JSON.stringify(this.value)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this)))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":case"Tab":return t.preventDefault(),this.input.autocomplete.set.call(this,e?e.textContent:null),!1}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}}}(jQuery);
/**
* Tagify (v 2.1.8)- tags input component
* Tagify (v 2.2.10)- tags input component
* By Yair Even-Or (2016)

@@ -16,949 +16,917 @@ * Don't sell this code. (c)

}(this, function() {
'use strict';
"use strict";
function Tagify(input, settings) {
// protection
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
// protection
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
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 = this.extend({}, this.DEFAULTS, settings);
this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
if (input.pattern) try {
this.settings.pattern = new RegExp(input.pattern);
if (input.pattern) try {
this.settings.pattern = new RegExp(input.pattern);
} catch (e) {} // Convert the "delimiters" setting into a REGEX object
if (this.settings && this.settings.delimiters) {
try {
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g");
} catch (e) {}
}
// Convert the "delimiters" setting into a REGEX object
if (this.settings && this.settings.delimiters) {
try {
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g");
} catch (e) {}
}
this.value = []; // tags' data
// events' callbacks references will be stores here, so events could be unbinded
this.value = []; // tags' data
this.listeners = {};
this.DOM = {}; // Store all relevant DOM elements in an Object
// 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.loadOriginalValues();
this.events.customBinding.call(this);
this.events.binding.call(this);
this.extend(this, new this.EventDispatcher(this));
this.build(input);
this.loadOriginalValues();
this.events.customBinding.call(this);
this.events.binding.call(this);
}
Tagify.prototype = {
isIE: window.document.documentMode,
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
autoComplete: true,
// Flag - tries to autocomplete the input's value while typing
dropdown: {
classname: '',
enabled: 2,
// minimum input characters needs to be typed for the dropdown to show
maxItems: 10,
itemTemplate: ''
}
},
customEventsList: ['add', 'remove', 'invalid'],
TEXTS: {
empty: "empty",
exceed: "number of tags exceeded",
pattern: "pattern mismatch",
duplicate: "already exists",
notAllowed: "not allowed"
},
/**
* 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;
},
// 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;
},
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
autoComplete: true, // Flag - tries to autocomplete the input's value while typing
mapValueToProp: "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value"
dropdown: {
classname: '',
enabled: 2, // minimum input characters needs to be typed for the dropdown to show
maxItems: 10,
itemTemplate: ''
}
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build: function build(input) {
var that = this,
DOM = this.DOM,
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>";
DOM.originalInput = input;
DOM.scope = this.parseHTML(template);
DOM.input = DOM.scope.querySelector('[contenteditable]');
input.parentNode.insertBefore(DOM.scope, input);
customEventsList: ['add', 'remove', 'invalid'],
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) {
this.dropdown.init.call(this);
}
/**
* 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");
input.autofocus && DOM.input.focus();
},
return node.body.firstElementChild;
},
/**
* Reverts back any changes made by this component
*/
destroy: function destroy() {
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
/**
* Merge two objects into a new one
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
*/
extend: function extend(o, o1, o2) {
if (!(o instanceof Object)) o = {};
copy(o, o1);
if (o2) copy(o, o2);
// 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;
},
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';
}
;
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build: function build(input) {
var that = this,
DOM = this.DOM,
template = '<tags class="tagify ' + input.className + '" ' + (this.settings.readonly ? 'readonly' : '') + '>\n <div contenteditable data-placeholder="' + input.placeholder + '" class="tagify__input"></div>\n </tags>';
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.settings.whitelist.length) {
this.dropdown.init.call(this);
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]);
} else a[key] = b[key];
}
}
}
input.autofocus && DOM.input.focus();
},
return o;
},
/**
* A constructor for exposing events to the outside
*/
EventDispatcher: function EventDispatcher(instance) {
// Create a DOM EventTarget object
var target = document.createTextNode(''); // Pass EventTarget interface calls to DOM EventTarget object
/**
* Reverts back any changes made by this component
*/
destroy: function destroy() {
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
this.off = function (name, cb) {
if (cb) target.removeEventListener.call(target, name, cb);
return this;
};
this.on = function (name, cb) {
if (cb) target.addEventListener.call(target, name, cb);
return this;
};
/**
* Merge two objects into a new one
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
*/
extend: function extend(o, o1, o2) {
if (!(o instanceof Object)) o = {};
this.trigger = function (eventName, data) {
var e;
if (!eventName) return;
copy(o, o1);
if (o2) copy(o, o2);
if (instance.settings.isJQueryPlugin) {
$(instance.DOM.originalInput).triggerHandler(eventName, [data]);
} else {
try {
e = new CustomEvent(eventName, {
"detail": data
});
} catch (err) {
console.warn(err);
}
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';
};
target.dispatchEvent(e);
}
};
},
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]);
} else a[key] = b[key];
}
}
}
/**
* DOM events listeners binding
*/
events: {
// bind custom events which were passed in the settings
customBinding: function customBinding() {
var _this2 = this;
return o;
this.customEventsList.forEach(function (name) {
_this2.on(name, _this2.settings.callbacks[name]);
});
},
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
var _CB = this.events.callbacks,
// setup callback references so events could be removed later
_CBR = this.listeners.main = this.listeners.main || {
paste: ['input', _CB.onPaste.bind(this)],
focus: ['input', _CB.onFocusBlur.bind(this)],
blur: ['input', _CB.onFocusBlur.bind(this)],
keydown: ['input', _CB.onKeydown.bind(this)],
click: ['scope', _CB.onClickScope.bind(this)]
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
/**
* A constructor for exposing events to the outside
*/
EventDispatcher: function EventDispatcher(instance) {
// Create a DOM EventTarget object
var target = document.createTextNode('');
for (var eventName in _CBR) {
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
// Pass EventTarget interface calls to DOM EventTarget object
this.off = function (name, cb) {
if (cb) target.removeEventListener.call(target, name, cb);
return this;
};
this.on = function (name, cb) {
if (cb) target.addEventListener.call(target, name, cb);
return this;
};
this.trigger = function (eventName, data) {
var e;
if (!eventName) return;
if (instance.settings.isJQueryPlugin) {
$(instance.DOM.originalInput).triggerHandler(eventName, [data]);
} else {
try {
e = new CustomEvent(eventName, { "detail": data });
} catch (err) {
console.warn(err);
}
target.dispatchEvent(e);
}
};
if (bindUnbind) {
// 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) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
}
},
/**
* DOM events listeners binding
* DOM events callbacks
*/
events: {
// bind custom events which were passed in the settings
customBinding: function customBinding() {
var _this2 = this;
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
this.customEventsList.forEach(function (name) {
_this2.on(name, _this2.settings.callbacks[name]);
});
},
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
if (this.settings.dropdown.enabled === 0) {
this.dropdown.show.call(this);
}
} else if (e.type == "blur" && s) {
this.settings.addTagOnBlur && this.addTags(s, true).length;
} else {
// e.target.classList.add('placeholder');
this.DOM.input.removeAttribute('style');
this.dropdown.hide.call(this);
}
},
onKeydown: function onKeydown(e) {
var s = e.target.textContent,
lastTag;
var _CB = this.events.callbacks,
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) {
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)');
lastTag = lastTag[lastTag.length - 1];
this.removeTag(lastTag);
} else if (e.key == 'Escape' || e.key == 'Esc') {
this.input.set.call(this);
e.target.blur();
} else if (e.key == 'Enter') {
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
// setup callback references so events could be removed later
_CBR = this.listeners.main = this.listeners.main || {
paste: ['input', _CB.onPaste.bind(this)],
focus: ['input', _CB.onFocusBlur.bind(this)],
blur: ['input', _CB.onFocusBlur.bind(this)],
keydown: ['input', _CB.onKeydown.bind(this)],
click: ['scope', _CB.onClickScope.bind(this)]
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
this.addTags(this.input.value, true);
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this);
},
onInput: function onInput(e) {
var value = this.input.normalize.call(this),
showSuggestions = value.length >= this.settings.dropdown.enabled;
for (var eventName in _CBR) {
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
if (!value) {
this.input.set.call(this, '');
return;
}
if (bindUnbind) {
// 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.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead
// save the value on the input's State object
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
}
},
this.input.set.call(this, value, false);
if (value.search(this.settings.delimiters) != -1) {
if (this.addTags(value).length) {
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);
}
},
onInputIE: function onInputIE(e) {
var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay
/**
* DOM events callbacks
*/
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
if (this.settings.dropdown.enabled === 0) {
this.dropdown.show.call(this);
}
} else if (e.type == "blur" && s) {
this.settings.addTagOnBlur && this.addTags(s, true).length;
} else {
// e.target.classList.add('placeholder');
this.DOM.input.removeAttribute('style');
this.dropdown.hide.call(this);
}
},
onKeydown: function onKeydown(e) {
var s = e.target.textContent,
lastTag;
setTimeout(function () {
_this.events.callbacks.onInput.call(_this, e);
});
},
onPaste: function onPaste(e) {},
onClickScope: function onClickScope(e) {
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") {
this.removeTag(e.target.parentNode);
}
}
}
},
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) {
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)');
lastTag = lastTag[lastTag.length - 1];
this.removeTag(lastTag);
} else if (e.key == 'Escape' || e.key == 'Esc') {
this.input.set.call(this);
e.target.blur();
} else if (e.key == 'Enter') {
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
this.addTags(this.input.value, true);
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this);
},
onInput: function onInput(e) {
var value = this.input.normalize.call(this),
showSuggestions = value.length >= this.settings.dropdown.enabled;
/**
* If the original input had an values, add them as tags
*/
loadOriginalValues: function loadOriginalValues() {
var value = this.DOM.originalInput.value; // if the original input already had any value (tags)
if (!value) {
this.input.set.call(this, '');
return;
}
if (!value) return;
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead
try {
value = JSON.parse(value);
} catch (err) {}
// save the value on the input's State object
this.input.set.call(this, value, false);
this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
},
if (value.search(this.settings.delimiters) != -1) {
if (this.addTags(value).length) {
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);
}
},
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);
});
},
onPaste: function onPaste(e) {},
onClickScope: function onClickScope(e) {
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") {
this.removeTag(e.target.parentNode);
}
}
}
/**
* 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;
this.input.value = s;
if (updateDOM) this.DOM.input.innerHTML = s;
if (!s) this.dropdown.hide.call(this);
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
this.input.validate.call(this);
},
// https://stackoverflow.com/a/3866442/104380
setRangeAtEnd: function setRangeAtEnd() {
var range, selection;
if (!document.createRange) return;
range = document.createRange();
range.selectNodeContents(this.DOM.input);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
/**
* If the original input had an values, add them as tags
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
*/
loadOriginalValues: function loadOriginalValues() {
var value = this.DOM.originalInput.value,
values;
validate: function validate() {
var isValid = !this.input.value || this.validateTag.call(this, this.input.value);
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() {
var clone = this.DOM.input.cloneNode(true),
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters
// if the original input already had any value (tags)
if (!value) return;
while (clone.firstElementChild) {
v += clone.firstElementChild.textContent;
clone.removeChild(clone.firstElementChild);
}
this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
return v.replace(/^\s+/, ""); // trimLeft
},
/**
* input bridge for accessing & setting
* @type {Object}
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
* @param {String} s [description]
*/
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;
autocomplete: {
suggest: function suggest(s) {
if (!s || !this.input.value) this.DOM.input.removeAttribute("data-suggest");else this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));
},
set: function set(s) {
var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null);
this.input.value = s;
if (suggestion) {
this.input.set.call(this, suggestion);
this.input.autocomplete.suggest.call(this, '');
this.dropdown.hide.call(this);
this.input.setRangeAtEnd.call(this);
} // if( suggestion && this.addTags(this.input.value + suggestion).length ){
// this.input.set.call(this);
// this.dropdown.hide.call(this);
// }
if (updateDOM) this.DOM.input.innerHTML = s;
if (!s) this.dropdown.hide.call(this);
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
}
}
},
getNodeIndex: function getNodeIndex(node) {
var index = 0;
this.input.validate.call(this);
},
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}
return index;
},
// https://stackoverflow.com/a/3866442/104380
setRangeAtEnd: function setRangeAtEnd() {
var range, selection;
/**
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {int} [Position index of the tag. -1 is returned if tag is not found.]
*/
isTagDuplicate: function isTagDuplicate(s) {
return this.value.findIndex(function (item) {
return s.trim().toLowerCase() === item.value.toLowerCase();
}); // return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
},
getTagIndexByValue: function getTagIndexByValue(value) {
var result = [];
this.DOM.scope.querySelectorAll('tag').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.DOM.scope.querySelectorAll('tag')[tagIdx];
},
if (!document.createRange) return;
/**
* 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) {
var tagsElms, tagsElmsLen;
tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined
range = document.createRange();
range.selectNodeContents(this.DOM.input);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 100);
return tagElm;
}
return false;
},
/**
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
*/
validate: function validate() {
var isValid = !this.input.value || this.validateTag.call(this, this.input.value);
this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true);
},
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted: function isTagBlacklisted(v) {
v = v.split(' ');
return this.settings.blacklist.filter(function (x) {
return v.indexOf(x) != -1;
}).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) {
var value = item.value ? item.value : item;
if (value.toLowerCase() === v.toLowerCase()) return true;
});
},
// remove any child DOM elements that aren't of type TEXT (like <br>)
normalize: function normalize() {
var clone = this.DOM.input.cloneNode(true),
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters
/**
* 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(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
result = true; // check for empty value
while (clone.firstElementChild) {
v += clone.firstElementChild.textContent;
clone.removeChild(clone.firstElementChild);
}
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; // check if pattern should be used and if so, use it to test the value
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed;
return result;
},
return v.replace(/^\s+/, ""); // trimLeft
},
/**
* 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 _this3 = this;
var whitelistWithProps = this.settings.whitelist[0] instanceof Object,
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0],
// checks if the value is a "complex" which means an Array of Objects, each object is a tag
temp = []; // no need to continue if "tagsItems" is an Array of Objects
/**
* suggest the rest of the input's value
* @param {String} s [description]
*/
autocomplete: {
suggest: function suggest(s) {
if (!this.DOM.input.value) return; // do not suggest anything for empty input
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest");
},
set: function set(s) {
var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null);
if (isComplex) return tagsItems; // if the value is a "simple" String, ex: "aaa, bbb, ccc"
if (suggestion) {
this.input.set.call(this, suggestion);
this.input.autocomplete.suggest.call(this, '');
this.dropdown.hide.call(this);
this.input.setRangeAtEnd.call(this);
}
if (typeof tagsItems == 'string') {
if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones)
// if( suggestion && this.addTags(this.input.value + suggestion).length ){
// this.input.set.call(this);
// this.dropdown.hide.call(this);
// }
}
}
},
return tagsItems.split(this.settings.delimiters).filter(function (n) {
return n;
}).map(function (v) {
return {
value: v.trim()
};
});
}
getNodeIndex: function getNodeIndex(node) {
var index = 0;
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}return index;
},
if (tagsItems instanceof Array) return tagsItems.map(function (v) {
return {
value: v.trim()
};
}); // search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
/**
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {int} [Position index of the tag. -1 is returned if tag is not found.]
*/
isTagDuplicate: function isTagDuplicate(s) {
return this.value.findIndex(function (item) {
return s.toLowerCase() === item.value.toLowerCase();
if (whitelistWithProps) {
tagsItems.forEach(function (tag) {
var matchObj = _this3.settings.whitelist.filter(function (WL_item) {
return WL_item.value.toLowerCase() == tag.value.toLowerCase();
});
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
},
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value
else temp.push(tag);
});
return temp;
}
},
/**
* 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) {
var tagsElms, tagsElmsLen, tagIdx;
/**
* 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]
* @return {Array} Array of DOM elements (tags)
*/
addTags: function addTags(tagsItems, clearInput) {
var _this4 = this;
if (!tagElm) {
tagIdx = this.isTagDuplicate.call(this, value);
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx];
}
var tagElems = [];
this.DOM.input.removeAttribute('style');
tagsItems = this.normalizeTags.call(this, tagsItems);
tagsItems.forEach(function (tagData) {
var tagValidation, tagElm;
// check AGAIN if "tagElm" is defined
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 100);
return tagElm;
}
if (typeof _this4.settings.transformTag === 'function') {
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value;
}
return false;
},
tagValidation = _this4.validateTag.call(_this4, tagData.value);
if (tagValidation !== true) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
tagData.title = tagValidation;
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted: function isTagBlacklisted(v) {
v = v.split(' ');
return this.settings.blacklist.filter(function (x) {
return v.indexOf(x) != -1;
}).length;
},
_this4.markTagByValue(tagData.value);
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagWhitelisted: function isTagWhitelisted(v) {
return this.settings.whitelist.some(function (item) {
var value = item.value ? item.value : item;
if (value.toLowerCase() === v.toLowerCase()) return true;
_this4.trigger("invalid", {
value: tagData.value,
index: _this4.value.length,
message: tagValidation
});
},
} // Create tag HTML element
/**
* 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(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
result = true;
tagElm = _this4.createTagElem(tagData);
tagElems.push(tagElm); // add the tag to the component's DOM
// check for empty value
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed;
appendTag.call(_this4, tagElm);
// check if pattern should be used and if so, use it to test the value
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern;
if (tagValidation === true) {
// update state
_this4.value.push(tagData);
// if duplicates are not allowed and there is a duplicate
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed;
_this4.update();
return result;
},
_this4.trigger('add', _this4.extend({}, {
index: _this4.value.length,
tag: tagElm
}, tagData));
} else if (!_this4.settings.keepInvalidTags) {
// remove invalid tags (if "keepInvalidTags" is set to "false")
setTimeout(function () {
_this4.removeTag(tagElm, true);
}, 1000);
}
});
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
/**
* 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]
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
normalizeTags: function normalizeTags(tagsItems) {
var _this3 = this;
var whitelistWithProps = this.settings.whitelist[0] instanceof Object,
isComplex = tagsItems instanceof Array && "value" in tagsItems[0],
// checks if the value is a "complex" which means an Array of Objects, each object is a tag
temp = [];
// no need to continue if "tagsItems" is an Array of Objects
if (isComplex) return tagsItems;
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 the value is a "simple" String, ex: "aaa, bbb, ccc"
if (!isComplex) {
if (!tagsItems.trim()) return [];
return tagElems;
},
// go over each tag and add it (if there were multiple ones)
tagsItems = tagsItems.split(this.settings.delimiters).filter(function (n) {
return n;
}).map(function (v) {
return { value: v.trim() };
});
}
/**
* 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 = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>";
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if (!isComplex && whitelistWithProps) {
tagsItems.forEach(function (tag) {
var matchObj = _this3.settings.whitelist.filter(function (WL_item) {
return WL_item.value.toLowerCase() == tag.value.toLowerCase();
});
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value
else temp.push(tag);
});
if (typeof this.settings.tagTemplate === "function") {
try {
template = this.settings.tagTemplate(v, tagData);
} catch (err) {}
} // for a certain Tag element, add attributes.
tagsItems = temp;
}
return tagsItems;
},
function addTagAttrs(tagElm, tagData) {
var i,
keys = Object.keys(tagData);
for (i = keys.length; i--;) {
var propName = keys[i];
if (!tagData.hasOwnProperty(propName)) return;
tagElm.setAttribute(propName, tagData[propName]);
}
}
/**
* 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]
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags]
* @return {Array} Array of DOM elements (tags)
*/
addTags: function addTags(tagsItems, clearInput) {
var _this4 = this;
tagElm = this.parseHTML(template); // add any attribuets, if exists
var tagElems = [];
addTagAttrs(tagElm, tagData);
return tagElm;
},
this.DOM.input.removeAttribute('style');
/**
* Removes a tag
* @param {Object|String} tagElm [DOM element or a String value]
* @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) {
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250;
if (!tagElm) return;
if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm);
var tagData,
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable)
tagsItems = this.normalizeTags.call(this, tagsItems);
if (tranDuration && tranDuration > 10) animation();else removeNode();
tagsItems.forEach(function (tagData) {
var tagValidation, tagElm;
if (!silent) {
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
if (typeof _this4.settings.transformTag === 'function') {
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value;
}
this.update(); // update the original input with the current value
tagValidation = _this4.validateTag.call(_this4, tagData.value);
this.trigger('remove', this.extend({}, {
index: tagIdx,
tag: tagElm
}, tagData));
}
if (tagValidation !== true) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
tagData.title = tagValidation;
_this4.markTagByValue.call(_this4, tagData.value);
_this4.trigger("invalid", { value: tagData.value, index: _this4.value.length, message: tagValidation });
}
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
// Create tag HTML element
tagElm = _this4.createTagElem(tagData);
tagElems.push(tagElm);
tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover)
// add the tag to the component's DOM
appendTag.call(_this4, tagElm);
setTimeout(removeNode, 400);
}
if (tagValidation === true) {
// update state
_this4.value.push(tagData);
_this4.update();
_this4.trigger('add', _this4.extend({}, { index: _this4.value.length, tag: tagElm }, tagData));
} else if (!_this4.settings.keepInvalidTags) {
// remove invalid tags (if "keepInvalidTags" is set to "false")
setTimeout(function () {
_this4.removeTag(tagElm, true);
}, 1000);
}
});
function removeNode() {
tagElm.parentNode.removeChild(tagElm);
}
},
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
},
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
/**
* update the origianl (hidden) input field's value
* see - https://stackoverflow.com/q/50957841/104380
*/
update: function update() {
this.DOM.originalInput.value = JSON.stringify(this.value);
},
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
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);
}
return tagElems;
/**
* Dropdown controller
* @type {Object}
*/
dropdown: {
init: function init() {
this.DOM.dropdown = this.dropdown.build.call(this);
},
build: function build() {
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(),
template = "<div class=\"" + className + "\"></div>";
return this.parseHTML(template);
},
show: function show(value) {
var listItems, listHTML;
if (!this.settings.whitelist.length) return; // if no value was supplied, show all the "whitelist" items in the dropdown
// @type [Array] listItems
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0);
listHTML = this.dropdown.createListHTML.call(this, listItems); // set the first item from the suggestions list as the autocomplete value
/**
* 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 = '<tag title=\'' + v + '\'>\n <x title=\'\'></x><div><span>' + v + '</span></div>\n </tag>';
if (this.settings.autoComplete) {
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : '');
} // if( !listHTML || listItems.length < 2 ){
// this.dropdown.hide.call(this);
// return;
// }
if (typeof this.settings.tagTemplate === "function") {
try {
template = this.settings.tagTemplate(v, tagData);
} catch (err) {}
}
// for a certain Tag element, add attributes.
function addTagAttrs(tagElm, tagData) {
var i,
keys = Object.keys(tagData);
for (i = keys.length; i--;) {
var propName = keys[i];
if (!tagData.hasOwnProperty(propName)) return;
tagElm.setAttribute(propName, tagData[propName]);
}
}
this.DOM.dropdown.innerHTML = listHTML;
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
tagElm = this.parseHTML(template);
if (!this.DOM.dropdown.parentNode != document.body) {
document.body.appendChild(this.DOM.dropdown);
this.events.binding.call(this, false); // unbind the main events
// add any attribuets, if exists
addTagAttrs(tagElm, tagData);
return tagElm;
this.dropdown.events.binding.call(this);
}
},
hide: function hide() {
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return;
document.body.removeChild(this.DOM.dropdown);
window.removeEventListener('resize', this.dropdown.position);
this.dropdown.events.binding.call(this, false); // unbind all events
/**
* Removes a tag
* @param {Object} tagElm [DOM element]
* @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) {
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250;
if (!tagElm) return;
var tagData,
tagIdx = this.getNodeIndex(tagElm);
if (!tagElm) return;
if (tranDuration && tranDuration > 10) animation();else removeNode();
if (!silent) {
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object
this.update(); // update the original input with the current value
this.trigger('remove', this.extend({}, { index: tagIdx, tag: tagElm }, tagData));
}
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, 400);
}
function removeNode() {
tagElm.parentNode.removeChild(tagElm);
}
this.events.binding.call(this); // re-bind main events
},
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
position: function position() {
var rect = this.DOM.scope.getBoundingClientRect();
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \
width: " + rect.width + "px";
},
/**
* update the origianl (hidden) input field's value
* see - https://stackoverflow.com/q/50957841/104380
* @type {Object}
*/
update: function update() {
var _this5 = this;
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]
* @return {[type]} [description]
*/
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
var tagsAsString = this.value.map(function (v) {
return v[_this5.settings.mapValueToProp || "value"] || v.value;
});
this.DOM.originalInput.value = JSON.stringify(tagsAsString);
},
/**
* Dropdown controller
* @type {Object}
*/
dropdown: {
init: function init() {
this.DOM.dropdown = this.dropdown.build.call(this);
// references to the ".bind()" methods must be saved so they could be unbinded later
var _CBR = this.listeners.dropdown = this.listeners.dropdown || {
position: this.dropdown.position.bind(this),
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this),
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this),
onClick: this.dropdown.events.callbacks.onClick.bind(this)
},
build: function build() {
var className = ('tagify__dropdown ' + this.settings.dropdown.classname).trim(),
template = '<div class="' + className + '"></div>';
return this.parseHTML(template);
},
show: function show(value) {
var listItems, listHTML;
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
if (!this.settings.whitelist.length) return;
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]('click', _CBR.onClick);
},
callbacks: {
onKeyDown: function onKeyDown(e) {
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],
newValue = "";
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0);
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Down': // >IE11
listHTML = this.dropdown.createListHTML.call(this, listItems);
case 'Up':
// >IE11
e.preventDefault();
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop
// set the first item from the suggestions list as the autocomplete value
if (this.settings.autoComplete) {
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : '');
}
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm, true);
break;
if (!listHTML || listItems.length < 2) {
this.dropdown.hide.call(this);
return;
}
case 'Escape':
case 'Esc':
// IE11
this.dropdown.hide.call(this);
break;
this.DOM.dropdown.innerHTML = listHTML;
this.dropdown.position.call(this);
case 'Enter':
e.preventDefault();
newValue = selectedElm ? selectedElm.textContent : this.input.value;
this.addTags(newValue, true);
this.dropdown.hide.call(this);
break;
// if the dropdown has yet to be appended to the document,
// append the dropdown to the body element & handle events
if (!this.DOM.dropdown.parentNode != document.body) {
document.body.appendChild(this.DOM.dropdown);
this.events.binding.call(this, false); // unbind the main events
this.dropdown.events.binding.call(this);
}
case 'ArrowRight':
case 'Tab':
e.preventDefault();
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null);
return false;
}
},
hide: function hide() {
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return;
document.body.removeChild(this.DOM.dropdown);
window.removeEventListener('resize', this.dropdown.position);
this.dropdown.events.binding.call(this, false); // unbind all events
this.events.binding.call(this); // re-bind main events
onMouseOver: function onMouseOver(e) {
// event delegation check
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target);
},
position: function position() {
var rect = this.DOM.scope.getBoundingClientRect();
onClick: function onClick(e) {
var _this5 = this;
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \
width: " + rect.width + "px";
},
var onClickOutside = function onClickOutside() {
return _this5.dropdown.hide.call(_this5);
},
listItemElm;
if (e.button != 0) return; // allow only mouse left-clicks
/**
* @type {Object}
*/
events: {
if (e.target == document.documentElement) return onClickOutside();
listItemElm = [e.target, e.target.parentNode].filter(function (a) {
return a.className.includes("tagify__dropdown__item");
})[0];
/**
* 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]
* @return {[type]} [description]
*/
binding: function binding() {
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
if (listItemElm) {
this.input.set.call(this);
this.addTags(listItemElm.textContent);
} // clicked outside the dropdown, so just close it
else onClickOutside();
}
}
},
highlightOption: function highlightOption(elm, adjustScroll) {
if (!elm) return;
var className = "tagify__dropdown__item--active"; // for IE support, which doesn't allow "forEach" on "NodeList" Objects
// references to the ".bind()" methods must be saved so they could be unbinded later
var _CBR = this.listeners.dropdown = this.listeners.dropdown || {
position: this.dropdown.position.bind(this),
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this),
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this),
onClick: this.dropdown.events.callbacks.onClick.bind(this)
},
action = bindUnbind ? 'addEventListener' : 'removeEventListener';
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) {
return activeElm.classList.remove(className);
}); // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
window[action]('resize', _CBR.position);
window[action]('keydown', _CBR.onKeyDown);
window[action]('mousedown', _CBR.onClick);
elm.classList.add(className);
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight;
},
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver);
// this.DOM.dropdown[action]('click', _CBR.onClick);
},
/**
* returns an HTML string of the suggestions' list items
* @return {[type]} [description]
*/
filterListItems: function filterListItems(value) {
if (!value) return "";
var list = [],
whitelist = this.settings.whitelist,
suggestionsCount = this.settings.dropdown.maxItems || Infinity,
whitelistItem,
valueIsInWhitelist,
i = 0;
for (; i < whitelist.length; i++) {
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : {
value: whitelist[i]
}, //normalize value as an Object
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">="
// match for the value within each "whitelist" item
callbacks: {
onKeyDown: function onKeyDown(e) {
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],
newValue = "";
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem);
if (suggestionsCount == 0) break;
}
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Down': // >IE11
case 'Up':
// >IE11
e.preventDefault();
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"];
return list;
},
// if no element was found, loop
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
/**
* Creates the dropdown items' HTML
* @param {Array} list [Array of Objects]
* @return {String}
*/
createListHTML: function createListHTML(list) {
var getItem = this.settings.dropdown.itemTemplate || function (item) {
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>";
}; // for a certain Tag element, add attributes.
this.dropdown.highlightOption.call(this, selectedElm, true);
break;
case 'Escape':
case 'Esc':
// IE11
this.dropdown.hide.call(this);
break;
function getAttributesString(item) {
var i,
keys = Object.keys(item),
s = "";
case 'Enter':
e.preventDefault();
newValue = selectedElm ? selectedElm.textContent : this.input.value;
this.addTags(newValue, true);
this.dropdown.hide.call(this);
break;
for (i = keys.length; i--;) {
var propName = keys[i];
if (propName != 'class' && !item.hasOwnProperty(propName)) return;
s += " " + propName + (item[propName] ? "=" + item[propName] : "");
}
case 'ArrowRight':
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null);
break;
}
},
onMouseOver: function onMouseOver(e) {
// event delegation check
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target);
},
onClick: function onClick(e) {
var _this6 = this;
return s;
}
var onClickOutside = function onClickOutside() {
return _this6.dropdown.hide.call(_this6);
},
listItemElm;
if (e.button != 0) return; // allow only mouse left-clicks
if (e.target == document.documentElement) return onClickOutside();
listItemElm = [e.target, e.target.parentNode].filter(function (a) {
return a.className.includes("tagify__dropdown__item");
})[0];
if (listItemElm) {
this.input.set.call(this);
this.addTags(listItemElm.textContent);
}
// clicked outside the dropdown, so just close it
else onClickOutside();
}
}
},
highlightOption: function highlightOption(elm, adjustScroll) {
if (!elm) return;
var className = "tagify__dropdown__item--active";
// for IE support, which doesn't allow "forEach" on "NodeList" Objects
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) {
return activeElm.classList.remove(className);
});
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight;
},
/**
* returns an HTML string of the suggestions' list items
* @return {[type]} [description]
*/
filterListItems: function filterListItems(value) {
if (!value) return "";
var list = [],
whitelist = this.settings.whitelist,
suggestionsCount = this.settings.dropdown.maxItems || Infinity,
whitelistItem,
valueIsInWhitelist,
i = 0;
for (; i < whitelist.length; i++) {
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value: whitelist[i] }, //normalize value as an Object
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">="
// match for the value within each "whitelist" item
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && 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(list) {
var getItem = this.settings.dropdown.itemTemplate || function (item) {
return '<div class=\'tagify__dropdown__item ' + (item.class ? item.class : "") + '\' ' + getAttributesString(item) + '>' + item.value + '</div>';
};
// for a certain Tag element, add attributes.
function getAttributesString(item) {
var i,
keys = Object.keys(item),
s = "";
for (i = keys.length; i--;) {
var propName = keys[i];
if (propName != 'class' && !item.hasOwnProperty(propName)) return;
s += " " + propName + (item[propName] ? "=" + item[propName] : "");
}
return s;
}
return list.map(getItem).join("");
}
return list.map(getItem).join("");
}
}
};
return Tagify;
}));

@@ -1,1 +0,1 @@

!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}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,autoComplete:!0,mapValueToProp:"",dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],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},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';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.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(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 s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)$(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&$(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;t&&this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){this.DOM.input.value&&(t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest"))},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.toLowerCase()===t.value.toLowerCase()})},markTagByValue:function(t,e){var i;return e||(i=this.isTagDuplicate.call(this,t),e=this.DOM.scope.querySelectorAll("tag")[i]),!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&"value"in t[0],s=[];if(n)return t;if(!n){if(!t.trim())return[];t=t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}})}return!n&&e&&(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),t=s),t},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue.call(n,t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){var n,s=this.getNodeIndex(t);t&&(i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n))))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){var e=this,t=this.value.map(function(t){return t[e.settings.mapValueToProp||"value"]||t.value});this.DOM.originalInput.value=JSON.stringify(t)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),!i||e.length<2?this.dropdown.hide.call(this):(this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this))))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":this.input.autocomplete.set.call(this,e?e.textContent:null)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}},t});
!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}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,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],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},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';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.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(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 s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)$(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&$(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;if(t){try{t=JSON.parse(t)}catch(t){}this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})}},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){t&&this.input.value?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.trim().toLowerCase()===t.value.toLowerCase()})},getTagIndexByValue:function(i){var n=[];return this.DOM.scope.querySelectorAll("tag").forEach(function(t,e){t.textContent.trim().toLowerCase()==i.toLowerCase()&&n.push(e)}),n},getTagElmByValue:function(t){var e=this.getTagIndexByValue(t)[0];return this.DOM.scope.querySelectorAll("tag")[e]},markTagByValue:function(t,e){return!!(e=e||this.getTagElmByValue(t))&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&t[0]instanceof Object&&"value"in t[0],s=[];return n?t:"string"==typeof t?t.trim()?t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}}):[]:t instanceof Array?t.map(function(t){return{value:t.trim()}}):e?(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),s):void 0},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue(t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){"string"==typeof t&&(t=this.getTagElmByValue(t));var n,s=this.getTagIndexByValue(t.textContent);i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n)))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=JSON.stringify(this.value)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this)))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":case"Tab":return t.preventDefault(),this.input.autocomplete.set.call(this,e?e.textContent:null),!1}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}},t});
{
"name": "@yaireo/tagify",
"version": "2.1.9",
"version": "2.3.0",
"homepage": "https://github.com/yairEO/tagify",

@@ -17,3 +17,3 @@ "description": "Want to simply convert an input field into a tags element, in a easy customizable way, with good performance and smallest code footprint? You are in the right place my friend.",

},
"main": "tagify.js",
"main": "./dist/tagify.js",
"repository": {

@@ -27,9 +27,11 @@ "type": "git",

"devDependencies": {
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"babel-core": "^6.26.3",
"babel-jest": "^23.4.2",
"babel-jest": "^23.6.0",
"babel-preset-env": "^1.7.0",
"beepbeep": "^1.2.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "4.0.0",
"gulp-babel": "^7.0.1",
"gulp-autoprefixer": "6.0.0",
"gulp-babel": "^8.0.0",
"gulp-cached": "^1.1.1",

@@ -40,19 +42,19 @@ "gulp-clean-css": "^3.10.0",

"gulp-css-globbing": "^0.2.2",
"gulp-eslint": "^3.0.1",
"gulp-eslint": "^5.0.0",
"gulp-insert": "^0.5.0",
"gulp-load-plugins": "^1.5.0",
"gulp-rename": "^1.2.3",
"gulp-replace": "^0.5.4",
"gulp-sass": "^3.2.1",
"gulp-rename": "^1.4.0",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify": "^3.0.1",
"gulp-umd": "^0.2.1",
"gulp-umd": "^2.0.0",
"gulp-util": "^3.0.8",
"gulp-watch": "^5.0.1",
"jest": "^23.5.0",
"jsdom": "^11.12.0",
"jest": "^23.6.0",
"jsdom": "^12.0.0",
"path": "^0.12.7",
"puppeteer": "^1.7.0",
"run-sequence": "^1.2.2"
"run-sequence": "^2.2.1",
"puppeteer": "^1.8.0"
}
}

@@ -21,2 +21,7 @@ [Tagify](https://yaireo.github.io/tagify) - lightweight input "tags" script

npm i @yaireo/tagify --save
// usage:
import Tagify from '@yaireo/tagify'
## [Documentation & Demos](https://yaireo.github.io/tagify)

@@ -47,3 +52,2 @@

## Building the project

@@ -111,5 +115,6 @@ Simply run `gulp` in your terminal, from the project's path ([Gulp](https://gulpjs.com) should be installed first).

// the original input's value is a String of Array items
// The original input's value is a String representing Array of Objects.
// To parse it, use: `JSON.parse(input.value)`
console.log( input.value )
// "["tag1", "tag2", ...]"
// "[{value:"tag1"}, {value:"tag2"}, ...]"
```

@@ -121,3 +126,3 @@

tagify.value
// [{ "value":"tag1", "class":"red", "id":1}, ...]
// [{"value":"tag1", "class":"red", "id":1}, ...]
```

@@ -166,4 +171,3 @@

tagify = new Tagify(input, {
whitelist : allowedTags,
mapValueToProp : "data-id"
whitelist : allowedTags
});

@@ -190,2 +194,10 @@

Another way to add tags is:
```javascript
tagify.addTags(
["banana", "orange", "apple"].map( item => ({ value:item }) )
);
```
### Suggestions selectbox

@@ -241,8 +253,10 @@ The suggestions selectbox is shown is a whitelist Array of Strings or Objects was passed in the settings when the Tagify instance was created.

Name | Info
--------------- | --------------------------------------------------------------------------
destroy | Reverts the input element back as it was before Tagify was applied
removeAllTags | Removes all tags and resets the original input tag's value property
addTags | Accepts a String (word, single or multiple with a delimiter) or an Array of Objects (see above)
removeTag | Removes a specific tag (argument is the tag DOM element to be removed. see source code.)
Name | Info
------------------- | --------------------------------------------------------------------------
destroy | Reverts the input element back as it was before Tagify was applied
removeAllTags | Removes all tags and resets the original input tag's value property
addTags | Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings
removeTag | Removes a specific tag (argument is the tag DOM element to be removed. see source code.)
getTagIndexByValue |
getTagElmByValue |

@@ -273,3 +287,2 @@

maxTags | Number | Infinity | Maximum number of tags
mapValueToProp | String | "" | For tags with properties, where a certain property should be used as the "saved" value
transformTag | Function | undefined | Takes a tag input as argument and returns a transformed value

@@ -276,0 +289,0 @@ tagTemplate | Function | undefined | Takes a tag's value and data as arguments and returns an HTML string for a tag element

@@ -39,3 +39,3 @@ function Tagify( input, settings ){

Tagify.prototype = {
isIE : window.document.documentMode,
isIE : window.document.documentMode, // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility

@@ -62,3 +62,2 @@ TEXTS : {

autoComplete : true, // Flag - tries to autocomplete the input's value while typing
mapValueToProp : "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value"
dropdown : {

@@ -332,4 +331,3 @@ classname : '',

loadOriginalValues(){
var value = this.DOM.originalInput.value,
values;
var value = this.DOM.originalInput.value;

@@ -339,2 +337,5 @@ // if the original input already had any value (tags)

try{ value = JSON.parse(value) }
catch(err){}
this.addTags(value).forEach(tag => {

@@ -397,3 +398,3 @@ tag && tag.classList.add('tagify--noAnim');

/**
* suggest the rest of the input's value
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
* @param {String} s [description]

@@ -403,5 +404,6 @@ */

suggest(s){
if( !this.DOM.input.value ) return; // do not suggest anything for empty input
if( s ) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));
else this.DOM.input.removeAttribute("data-suggest");
if( !s || !this.input.value )
this.DOM.input.removeAttribute("data-suggest");
else
this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));
},

@@ -441,6 +443,20 @@ set(s){

isTagDuplicate(s){
return this.value.findIndex(item => s.toLowerCase() === item.value.toLowerCase());
return this.value.findIndex(item => s.trim().toLowerCase() === item.value.toLowerCase());
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
},
getTagIndexByValue( value ){
var result = [];
this.DOM.scope.querySelectorAll('tag').forEach((tagElm, i) => {
if( tagElm.textContent.trim().toLowerCase() == value.toLowerCase() )
result.push(i)
})
return result;
},
getTagElmByValue( value ){
var tagIdx = this.getTagIndexByValue(value)[0];
return this.DOM.scope.querySelectorAll('tag')[tagIdx];
},
/**

@@ -453,8 +469,5 @@ * Mark a tag element by its value

markTagByValue(value, tagElm){
var tagsElms, tagsElmsLen, tagIdx
var tagsElms, tagsElmsLen
if( !tagElm ){
tagIdx = this.isTagDuplicate.call(this, value);
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx];
}
tagElm = tagElm || this.getTagElmByValue(value);

@@ -528,5 +541,5 @@ // check AGAIN if "tagElm" is defined

*/
normalizeTags(tagsItems){
normalizeTags( tagsItems ){
var whitelistWithProps = this.settings.whitelist[0] instanceof Object,
isComplex = tagsItems instanceof Array && "value" in tagsItems[0], // checks if the value is a "complex" which means an Array of Objects, each object is a tag
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0], // checks if the value is a "complex" which means an Array of Objects, each object is a tag
temp = [];

@@ -540,11 +553,14 @@

// if the value is a "simple" String, ex: "aaa, bbb, ccc"
if( !isComplex ){
if( typeof tagsItems == 'string' ){
if( !tagsItems.trim() ) return [];
// go over each tag and add it (if there were multiple ones)
tagsItems = tagsItems.split(this.settings.delimiters).filter(n => n).map(v => ({ value:v.trim() }));
return tagsItems.split(this.settings.delimiters).filter(n => n).map(v => ({ value:v.trim() }));
}
if( tagsItems instanceof Array )
return tagsItems.map(v => ({ value:v.trim() }))
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if( !isComplex && whitelistWithProps ){
if( whitelistWithProps ){
tagsItems.forEach(tag => {

@@ -558,7 +574,4 @@ var matchObj = this.settings.whitelist.filter( WL_item => WL_item.value.toLowerCase() == tag.value.toLowerCase() )

tagsItems = temp;
return temp;
}
return tagsItems;
},

@@ -568,3 +581,3 @@

* 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]
* @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]

@@ -592,3 +605,3 @@ * @return {Array} Array of DOM elements (tags)

tagData.title = tagValidation;
this.markTagByValue.call(this, tagData.value);
this.markTagByValue(tagData.value);
this.trigger("invalid", {value:tagData.value, index:this.value.length, message:tagValidation});

@@ -675,5 +688,5 @@ }

* Removes a tag
* @param {Object} tagElm [DOM element]
* @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]
* @param {Object|String} tagElm [DOM element or a String value]
* @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]
*/

@@ -683,7 +696,8 @@ removeTag( tagElm, silent, tranDuration = 250 ){

if( typeof tagElm == 'string' )
tagElm = this.getTagElmByValue(tagElm)
var tagData,
tagIdx = this.getNodeIndex(tagElm);
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable)
if( !tagElm) return;
if( tranDuration && tranDuration > 10 ) animation()

@@ -723,4 +737,3 @@ else removeNode();

update(){
var tagsAsString = this.value.map(v => v[this.settings.mapValueToProp || "value"] || v.value);
this.DOM.originalInput.value = JSON.stringify(tagsAsString);
this.DOM.originalInput.value = JSON.stringify(this.value);
},

@@ -748,2 +761,4 @@

// if no value was supplied, show all the "whitelist" items in the dropdown
// @type [Array] listItems
listItems = value ?

@@ -761,6 +776,6 @@ this.dropdown.filterListItems.call(this, value) :

if( !listHTML || listItems.length < 2 ){
this.dropdown.hide.call(this);
return;
}
// if( !listHTML || listItems.length < 2 ){
// this.dropdown.hide.call(this);
// return;
// }

@@ -859,4 +874,6 @@ this.DOM.dropdown.innerHTML = listHTML

case 'ArrowRight' :
case 'Tab' :
e.preventDefault();
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null);
break;
return false;
}

@@ -863,0 +880,0 @@ },

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc