Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@yaireo/tagify

Package Overview
Dependencies
Maintainers
1
Versions
266
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 1.2.2 to 2.0.0

dist/tagify.polyfills.js

1099

dist/jQuery.tagify.js

@@ -0,3 +1,5 @@

"use strict";
/**
* Tagify (v 1.2.1)- tags input component
* Tagify (v 1.3.1)- tags input component
* By Yair Even-Or (2016)

@@ -7,437 +9,844 @@ * Don't sell this code. (c)

*/
;(function($){
;(function ($) {
// just a jQuery wrapper for the vanilla version of this component
$.fn.tagify = function(settings){
var $input = this,
tagify;
$.fn.tagify = function () {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if( $input.data("tagify") ) // don't continue if already "tagified"
return this;
return this.each(function () {
var $input = $(this),
tagify;
tagify = new Tagify(this[0], settings);
tagify.isJQueryPlugin = true;
$input.data("tagify", tagify);
if ($input.data("tagify")) // don't continue if already "tagified"
return this;
return this;
}
settings.isJQueryPlugin = true;
tagify = new Tagify($input[0], settings);
$input.data("tagify", tagify);
});
};
function Tagify( input, settings ){
// protection
if( !input ){
console.warn('Tagify: ', 'invalid input element ', input)
return this;
}
function Tagify(input, settings) {
// protection
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
this.settings = this.extend({}, settings, this.DEFAULTS);
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( input.pattern )
try {
if (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){}
} catch (e) {}
if( settings && settings.delimiters ){
try {
this.settings.delimiters = new RegExp("[" + settings.delimiters + "]");
} 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.id = Math.random().toString(36).substr(2, 9), // almost-random ID (because, fuck it)
this.value = []; // An array holding all the (currently used) tags
// 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.events.customBinding.call(this);
this.events.binding.call(this);
}
this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it)
this.value = []; // An array holding all the (currently used) tags
this.DOM = {}; // Store all relevant DOM elements in an Object
this.extend(this, new this.EventDispatcher());
this.build(input);
this.events();
}
Tagify.prototype = {
isIE: window.document.documentMode,
Tagify.prototype = {
DEFAULTS : {
delimiters : ",", // [regex] split tags by any of these delimiters
pattern : "", // pattern to validate input by
callbacks : {}, // exposed callbacks object to be triggered on certain events
duplicates : false, // flag - allow tuplicate tags
enforeWhitelist : false, // flag - should ONLY use tags allowed in whitelist
autocomplete : true, // flag - show native suggeestions list as you type
whitelist : [], // is this list has any items, then only allow tags from this list
blacklist : [], // a list of non-allowed tags
maxTags : Infinity, // maximum number of tags
suggestionsMinChars : 2 // minimum characters to input to see sugegstions list
},
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: [], // is this list has any items, then only allow tags from this list
blacklist: [], // a list of non-allowed tags
enforceWhitelist: false, // flag - should ONLY use tags allowed in whitelist
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
}
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build : function( input ){
var that = this,
value = input.value,
inputHTML = '<div><input list="tagifySuggestions'+ this.id +'" class="placeholder"/><span>'+ input.placeholder +'</span></div>';
this.DOM.originalInput = input;
this.DOM.scope = document.createElement('tags');
this.DOM.scope.innerHTML = inputHTML;
this.DOM.input = this.DOM.scope.querySelector('input');
customEventsList: ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'],
if( this.settings.readonly )
this.DOM.scope.classList.add('readonly')
/**
* 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.parentNode.insertBefore(this.DOM.scope, input);
this.DOM.scope.appendChild(input);
return node.body.firstElementChild;
},
// if "autocomplete" flag on toggeled & "whitelist" has items, build suggestions list
if( this.settings.autocomplete && this.settings.whitelist.length )
this.DOM.datalist = this.buildDataList();
// if the original input already had any value (tags)
if( value )
this.addTag(value).forEach(function(tag){
// https://stackoverflow.com/a/25396011/104380
escapeHtml: function escapeHtml(s) {
var text = document.createTextNode(s);
var p = document.createElement('p');
p.appendChild(text);
return p.innerHTML;
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build: function build(input) {
var that = this,
value = input.value,
template = "\n <tags class=\"tagify " + input.className + " " + (this.settings.readonly ? 'readonly' : '') + "\">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify--input\"></div>\n </tags>";
this.DOM.originalInput = input;
this.DOM.scope = this.parseHTML(template);
this.DOM.input = this.DOM.scope.querySelector('[contenteditable]');
input.parentNode.insertBefore(this.DOM.scope, input);
// if "autocomplete" flag on toggeled & "whitelist" has items, build suggestions list
if (this.settings.dropdown.enabled && this.settings.whitelist.length) {
this.dropdown.init.call(this);
}
// if the original input already had any value (tags)
if (value) this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
},
},
/**
* Reverts back any changes made by this component
*/
destroy : function(){
this.DOM.scope.parentNode.appendChild(this.DOM.originalInput);
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
/**
* Merge two objects into a new one
*/
extend : function(o, o1, o2){
if( !(o instanceof Object) ) o = {};
/**
* Reverts back any changes made by this component
*/
destroy: function destroy() {
this.DOM.scope.parentNode.removeChild(this.DOM.scope);
},
if( o2 ){
copy(o, o2)
copy(o, o1)
}
else
copy(o, o1)
function copy(a,b){
// copy o2 to o
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
}
/**
* 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 = {};
return o;
},
copy(o, o1);
if (o2) copy(o, o2);
/**
* A constructor for exposing events to the outside
*/
EventDispatcher : function(){
// Create a DOM EventTarget object
var target = document.createTextNode('');
function isObject(obj) {
var type = Object.prototype.toString.call(obj);
return obj === Object(obj) && type != '[object Array]' && type != '[object Function]';
};
// Pass EventTarget interface calls to DOM EventTarget object
this.off = target.removeEventListener.bind(target);
this.on = target.addEventListener.bind(target);
this.trigger = function(eventName, data){
var e;
if( !eventName ) return;
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];
}
}
}
if( this.isJQueryPlugin )
$(this.DOM.originalInput).triggerHandler(eventName, [data])
else{
try {
e = new CustomEvent(eventName, {"detail":data});
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
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);
}
catch(err){
e = document.createEvent("Event");
e.initEvent("toggle", false, false);
};
},
/**
* 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;
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]);
}
target.dispatchEvent(e);
}
}
},
/**
* DOM events listeners binding
*/
events : function(){
var that = this,
events = {
// event name / event callback / element to be listening to
paste : ['onPaste' , 'input'],
focus : ['onFocusBlur' , 'input'],
blur : ['onFocusBlur' , 'input'],
input : ['onInput' , 'input'],
keydown : ['onKeydown' , 'input'],
click : ['onClickScope' , 'scope']
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));
}
},
customList = ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'];
for( var e in events )
this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].bind(this));
customList.forEach(function(name){
that.on(name, that.settings.callbacks[name])
})
/**
* DOM events callbacks
*/
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
if( this.isJQueryPlugin )
$(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))
},
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
} 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;
/**
* DOM events callbacks
*/
callbacks : {
onFocusBlur : function(e){
var text = e.target.value.trim();
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') {
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 = e.target.textContent.trim(),
showSuggestions = value.length >= this.settings.dropdown.enabled;
if( e.type == "focus" )
e.target.className = 'input';
else if( e.type == "blur" && text ){
if( this.addTag(text).length )
e.target.value = '';
if (this.input.value == value) return;
// save the value on the input state object
this.input.value = value;
this.input.normalize.call(this);
this.input.autocomplete.suggest.call(this, ''); // cleanup any possible previous suggestion
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 && this.settings.whitelist.length) 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);
}
}
}
else{
e.target.className = 'input placeholder';
this.DOM.input.removeAttribute('style');
}
},
onKeydown : function(e){
var s = e.target.value,
that = this;
/**
* input bridge for accessing & setting
* @type {Object}
*/
input: {
value: '',
set: function set() {
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
if( e.key == "Backspace" && (s == "" || s.charCodeAt(0) == 8203) ){
this.removeTag( this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)').length - 1 );
this.input.value = this.DOM.input.innerHTML = s;
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
},
// remove any child DOM elements that aren't of type TEXT (like <br>)
normalize: function normalize() {
while (this.DOM.input.firstElementChild) {
this.DOM.input.removeChild(this.DOM.input.firstElementChild);
}
},
/**
* suggest the rest of the input's value
* @param {String} s [description]
*/
autocomplete: {
suggest: function suggest(s) {
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest");
},
set: function set() {
var suggestion = this.DOM.input.getAttribute('data-suggest');
if (suggestion && this.addTags(this.input.value + suggestion).length) {
this.input.set.call(this);
this.dropdown.hide.call(this);
}
}
}
if( e.key == "Escape" ){
e.target.value = '';
e.target.blur();
}
if( e.key == "Enter" ){
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
if( this.addTag(s).length )
e.target.value = '';
return false;
}
else{
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
}
},
onInput : function(e){
var value = e.target.value,
lastChar = value[value.length - 1],
isDatalistInput = !this.noneDatalistInput && value.length > 1,
showSuggestions = value.length >= this.settings.suggestionsMinChars,
datalistInDOM;
getNodeIndex: function getNodeIndex(node) {
var index = 0;
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}return index;
},
e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';
/**
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {boolean} [found / not found]
*/
isTagDuplicate: function isTagDuplicate(s) {
return this.value.some(function (item) {
return s.toLowerCase() === item.value.toLowerCase();
});
},
// if( value.indexOf(',') != -1 || isDatalistInput ){
if( value.slice().search(this.settings.delimiters) != -1 || isDatalistInput ){
if( this.addTag(value).length )
e.target.value = ''; // clear the input field's value
}
else if( this.settings.autocomplete && this.settings.whitelist.length ){
datalistInDOM = this.DOM.input.parentNode.contains( this.DOM.datalist );
// if sugegstions should be hidden
if( !showSuggestions && datalistInDOM )
this.DOM.input.parentNode.removeChild(this.DOM.datalist)
else if( showSuggestions && !datalistInDOM ){
this.DOM.input.parentNode.appendChild(this.DOM.datalist)
/**
* 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;
if (!tagElm) {
tagsElms = this.DOM.scope.querySelectorAll('tag');
for (tagsElmsLen = tagsElms.length; tagsElmsLen--;) {
if (tagsElms[tagsElmsLen].value.toLowerCase().includes(value.toLowerCase())) tagElm = tagsElms[tagsElmsLen];
}
}
// check AGAIN if "tagElm" is defined
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 2000);
return true;
} else {}
return false;
},
onPaste : function(e){
var that = this;
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
/**
* 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;
},
onClickScope : function(e){
if( e.target.tagName == "TAGS" )
this.DOM.input.focus();
if( e.target.tagName == "X" ){
this.removeTag( this.getNodeIndex(e.target.parentNode) );
}
}
},
/**
* Build tags suggestions using HTML datalist
* @return {[type]} [description]
*/
buildDataList : function(){
var OPTIONS = "",
i,
datalist = document.createElement('datalist');
/**
* 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;
});
},
datalist.id = 'tagifySuggestions' + this.id;
datalist.innerHTML = "<label> \
select from the list: \
<select> \
<option value=''></option> \
[OPTIONS] \
</select> \
</label>";
for( i=this.settings.whitelist.length; i--; )
OPTIONS += "<option>"+ this.settings.whitelist[i] +"</option>";
/**
* 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 that = this,
tagElems = [];
datalist.innerHTML = datalist.innerHTML.replace('[OPTIONS]', OPTIONS); // inject the options string in the right place
this.DOM.input.removeAttribute('style');
// this.DOM.input.insertAdjacentHTML('afterend', datalist); // append the datalist HTML string in the Tags
/**
* 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]
*/
function 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
result = tagsItems; // the returned result
return datalist;
},
// no need to continue if "tagsItems" is an Array of Objects
if (isComplex) return result;
getNodeIndex : function( node ){
var index = 0;
while( (node = node.previousSibling) )
if (node.nodeType != 3 || !/^\s*$/.test(node.data))
index++;
return index;
},
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if (!isComplex && typeof tagsItems == "string" && whitelistWithProps) {
var matchObj = this.settings.whitelist.filter(function (item) {
return item.value.toLowerCase() == tagsItems.toLowerCase();
});
/**
* Searches if any tags with a certain value exist and mark them
* @param {String / Number} value [description]
* @return {boolean} [found / not found]
*/
markTagByValue : function(value){
var idx = this.value.filter(function(item){ return value.toLowerCase() === item.toLowerCase() })[0],
tag = this.DOM.scope.querySelectorAll('tag')[idx];
if (matchObj[0]) {
isComplex = true;
result = matchObj; // set the Array (with the found Object) as the new value
}
}
if( tag ){
tag.classList.add('tagify--mark');
setTimeout(function(){ tag.classList.remove('tagify--mark') }, 2000);
return true;
}
// if the value is a "simple" String, ex: "aaa, bbb, ccc"
if (!isComplex) {
tagsItems = tagsItems.trim();
if (!tagsItems) return [];
return false;
},
// go over each tag and add it (if there were multiple ones)
result = tagsItems.split(this.settings.delimiters).map(function (v) {
return { value: v.trim() };
});
}
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted : function(v){
v = v.split(' ');
return this.settings.blacklist.filter(function(x){ return v.indexOf(x) != -1 }).length;
},
return result.filter(function (n) {
return n;
}); // cleanup the array from "undefined", "false" or empty items;
}
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagWhitelisted : function(v){
return this.settings.whitelist.indexOf(v) != -1;
},
/**
* validate a tag object BEFORE the actual tag will be created & appeneded
* @param {Object} tagData [{"value":"text", "class":whatever", ...}]
* @return {Boolean/String} ["true" if validation has passed, String or "false" for any type of error]
*/
function validateTag(tagData) {
var value = tagData.value.trim(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
tagAllowed;
/**
* add a "tag" element to the "tags" component
* @param {String} value [A string of a value or multiple values]
* @return {Array} Array of DOM elements
*/
addTag : function( value ){
var that = this,
result;
// check for empty value
if (!value) return "empty";
this.DOM.input.removeAttribute('style');
// check if pattern should be used and if so, use it to test the value
if (this.settings.pattern && !this.settings.pattern.test(value)) return "pattern";
value = value.trim();
// check if the tag already exists
if (this.isTagDuplicate(value)) {
this.trigger('duplicate', value);
if( !value ) return [];
if (!this.settings.duplicates) {
// this.markTagByValue(value, tagElm)
return "duplicate";
}
}
// go over each tag and add it (if there were multiple ones)
result = value.split(this.settings.delimiters).filter(function(v){ return !!v }).map(function(v){
v = v.trim();
// check if the tag is allowed by the rules set
tagAllowed = !this.isTagBlacklisted(value) && (!this.settings.enforceWhitelist || this.isTagWhitelisted(value)) && !maxTagsExceed;
if( that.settings.pattern && !(that.settings.pattern.test(v)) )
return false;
// Check against blacklist & whitelist (if enforced)
if (!tagAllowed) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
var tagElm = document.createElement('tag'),
isDuplicate = that.markTagByValue(v),
tagAllowed,
tagNotAllowedEventName,
maxTagsExceed = that.value.length >= that.settings.maxTags;
// broadcast why the tag was not allowed
if (maxTagsExceed) eventName__error = 'maxTagsExceed';else if (this.isTagBlacklisted(value)) eventName__error = 'blacklisted';else if (this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) eventName__error = 'notWhitelisted';
if( isDuplicate ){
that.trigger('duplicate', v);
if( !that.settings.duplicates ){
return false;
this.trigger(eventName__error, { value: value, index: this.value.length });
return "notAllowed";
}
return true;
}
tagAllowed = !that.isTagBlacklisted(v) && (!that.settings.enforeWhitelist || that.isTagWhitelisted(v)) && !maxTagsExceed;
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
function appendTag(tagElm) {
this.DOM.scope.insertBefore(tagElm, this.DOM.input);
}
// check against blacklist & whitelist (if enforced)
if( !tagAllowed ){
tagElm.classList.add('tagify--notAllowed');
setTimeout(function(){ that.removeTag(that.getNodeIndex(tagElm), true) }, 1000);
//////////////////////
tagsItems = normalizeTags.call(this, tagsItems);
// broadcast why the tag was not allowed
if( maxTagsExceed ) tagNotAllowedEventName = 'maxTagsExceed';
else if( that.isTagBlacklisted(v) ) tagNotAllowedEventName = 'blacklisted';
else if( that.settings.enforeWhitelist && !that.isTagWhitelisted(v) ) tagNotAllowedEventName = 'notWhitelisted';
tagsItems.forEach(function (tagData) {
var isTagValidated = validateTag.call(that, tagData);
that.trigger(tagNotAllowedEventName, {value:v, index:that.value.length});
if (isTagValidated === true || isTagValidated == "notAllowed") {
// create the tag element
var tagElm = that.createTagElem(tagData);
// add the tag to the component's DOM
appendTag.call(that, tagElm);
// remove the tag "slowly"
if (isTagValidated == "notAllowed") {
setTimeout(function () {
that.removeTag(tagElm, true);
}, 1000);
} else {
// update state
that.value.push(tagData);
that.update();
that.trigger('add', that.extend({}, { index: that.value.length, tag: tagElm }, tagData));
tagElems.push(tagElm);
}
}
});
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
// the space below is important - http://stackoverflow.com/a/19668740/104380
tagElm.innerHTML = "<x></x><div><span title='"+ v +"'>"+ v +" </span></div>";
that.DOM.scope.insertBefore(tagElm, that.DOM.input.parentNode);
return tagElems;
},
if( tagAllowed ){
that.value.push(v);
that.update();
that.trigger('add', {value:v, index:that.value.length});
/**
* 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,
escapedValue = this.escapeHtml(tagData.value),
template = "<tag>\n <x></x><div><span title='" + escapedValue + "'>" + escapedValue + "</span></div>\n </tag>";
// 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);
// add any attribuets, if exists
addTagAttrs(tagElm, tagData);
return tagElm;
});
},
return result.filter(function(n){ return n });
},
/**
* Removes a tag
* @param {Number} idx [tag index to be removed]
* @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]
*/
removeTag : function( idx, silent ){
var tagElm = this.DOM.scope.children[idx];
if( !tagElm) return;
/**
* 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]
*/
removeTag: function removeTag(tagElm, silent) {
var tagData,
tagIdx = this.getNodeIndex(tagElm);
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 (!tagElm) return;
// manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(function(){
tagElm.parentNode.removeChild(tagElm);
}, 400);
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( !silent ){
this.value.splice(idx, 1); // remove the tag from the data object
this.update(); // update the original input with the current value
this.trigger('remove', {value:tagElm.textContent.trim(), index:idx});
}
},
// manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(function () {
tagElm.parentNode.removeChild(tagElm);
}, 400);
removeAllTags : function(){
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function(elm){
elm.parentNode.removeChild(elm);
});
},
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));
}
},
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
},
/**
* update the origianl (hidden) input field's value
*/
update : function(){
this.DOM.originalInput.value = this.value.join(',');
}
}
})(jQuery);
/**
* update the origianl (hidden) input field's value
*/
update: function update() {
var tagsAsString = this.value.map(function (v) {
return v.value;
}).join(',');
this.DOM.originalInput.value = tagsAsString;
},
/**
* 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 = this.dropdown.filterListItems.call(this, value),
listHTML = this.dropdown.createListHTML(listItems);
if (listItems.length && this.settings.autoComplete) this.input.autocomplete.suggest.call(this, listItems[0].value);
if (!listHTML || listItems.length < 2) {
this.dropdown.hide.call(this);
return;
}
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 (!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;
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
},
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;
// 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';
window[action]('resize', _CBR.position);
window[action]('keydown', _CBR.onKeyDown);
window[action]('click', _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 = "";
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' ? "previousElementSibling" : "nextElementSibling"];
// if no element was found, loop
else selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm);
break;
case 'Escape':
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);
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) {
if (e.target.className.includes('tagify__dropdown__item')) {
this.input.set.call(this);
this.addTags(e.target.textContent);
}
// clicked outside the dropdown, so just close it
this.dropdown.hide.call(this);
}
}
},
highlightOption: function highlightOption(elm) {
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) {
activeElm.classList.remove(className);
});
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
},
/**
* 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) && 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) {
function getItem(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("");
}
}
};
})(jQuery);

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

!function(t){function e(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},e,this.DEFAULTS),this.settings.readonly=t.hasAttribute("readonly"),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(e&&e.delimiters)try{this.settings.delimiters=new RegExp("["+e.delimiters+"]")}catch(t){}this.id=Math.random().toString(36).substr(2,9),this.value=[],this.DOM={},this.extend(this,new this.EventDispatcher),this.build(t),this.events()}t.fn.tagify=function(t){var i,n=this;return n.data("tagify")?this:(i=new e(this[0],t),i.isJQueryPlugin=!0,n.data("tagify",i),this)},e.prototype={DEFAULTS:{delimiters:",",pattern:"",callbacks:{},duplicates:!1,enforeWhitelist:!1,autocomplete:!0,whitelist:[],blacklist:[],maxTags:1/0,suggestionsMinChars:2},build:function(t){var e=t.value,i='<div><input list="tagifySuggestions'+this.id+'" class="placeholder"/><span>'+t.placeholder+"</span></div>";this.DOM.originalInput=t,this.DOM.scope=document.createElement("tags"),this.DOM.scope.innerHTML=i,this.DOM.input=this.DOM.scope.querySelector("input"),this.settings.readonly&&this.DOM.scope.classList.add("readonly"),t.parentNode.insertBefore(this.DOM.scope,t),this.DOM.scope.appendChild(t),this.settings.autocomplete&&this.settings.whitelist.length&&(this.DOM.datalist=this.buildDataList()),e&&this.addTag(e).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},destroy:function(){this.DOM.scope.parentNode.appendChild(this.DOM.originalInput),this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}return t instanceof Object||(t={}),i?(n(t,i),n(t,e)):n(t,e),t},EventDispatcher:function(){var e=document.createTextNode("");this.off=e.removeEventListener.bind(e),this.on=e.addEventListener.bind(e),this.trigger=function(i,n){var s;if(i)if(this.isJQueryPlugin)t(this.DOM.originalInput).triggerHandler(i,[n]);else{try{s=new CustomEvent(i,{detail:n})}catch(t){(s=document.createEvent("Event")).initEvent("toggle",!1,!1)}e.dispatchEvent(s)}}},events:function(){var e=this,i={paste:["onPaste","input"],focus:["onFocusBlur","input"],blur:["onFocusBlur","input"],input:["onInput","input"],keydown:["onKeydown","input"],click:["onClickScope","scope"]},n=["add","remove","duplicate","maxTagsExceed","blacklisted","notWhitelisted"];for(var s in i)this.DOM[i[s][1]].addEventListener(s,this.callbacks[i[s][0]].bind(this));n.forEach(function(t){e.on(t,e.settings.callbacks[t])}),this.isJQueryPlugin&&t(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))},callbacks:{onFocusBlur:function(t){var e=t.target.value.trim();"focus"==t.type?t.target.className="input":"blur"==t.type&&e?this.addTag(e).length&&(t.target.value=""):(t.target.className="input placeholder",this.DOM.input.removeAttribute("style"))},onKeydown:function(t){var e=t.target.value,i=this;if("Backspace"!=t.key||""!=e&&8203!=e.charCodeAt(0)||this.removeTag(this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)").length-1),"Escape"==t.key&&(t.target.value="",t.target.blur()),"Enter"==t.key)return t.preventDefault(),this.addTag(e).length&&(t.target.value=""),!1;this.noneDatalistInput&&clearTimeout(this.noneDatalistInput),this.noneDatalistInput=setTimeout(function(){i.noneDatalistInput=null},50)},onInput:function(t){var e,i=t.target.value,n=(i[i.length-1],!this.noneDatalistInput&&i.length>1),s=i.length>=this.settings.suggestionsMinChars;t.target.style.width=7*(t.target.value.length+1)+"px",-1!=i.slice().search(this.settings.delimiters)||n?this.addTag(i).length&&(t.target.value=""):this.settings.autocomplete&&this.settings.whitelist.length&&(e=this.DOM.input.parentNode.contains(this.DOM.datalist),!s&&e?this.DOM.input.parentNode.removeChild(this.DOM.datalist):s&&!e&&this.DOM.input.parentNode.appendChild(this.DOM.datalist))},onPaste:function(t){var e=this;this.noneDatalistInput&&clearTimeout(this.noneDatalistInput),this.noneDatalistInput=setTimeout(function(){e.noneDatalistInput=null},50)},onClickScope:function(t){"TAGS"==t.target.tagName&&this.DOM.input.focus(),"X"==t.target.tagName&&this.removeTag(this.getNodeIndex(t.target.parentNode))}},buildDataList:function(){var t,e="",i=document.createElement("datalist");for(i.id="tagifySuggestions"+this.id,i.innerHTML="<label> select from the list: <select> <option value=''></option> [OPTIONS] </select> </label>",t=this.settings.whitelist.length;t--;)e+="<option>"+this.settings.whitelist[t]+"</option>";return i.innerHTML=i.innerHTML.replace("[OPTIONS]",e),i},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},markTagByValue:function(t){var e=this.value.filter(function(e){return t.toLowerCase()===e.toLowerCase()})[0],i=this.DOM.scope.querySelectorAll("tag")[e];return!!i&&(i.classList.add("tagify--mark"),setTimeout(function(){i.classList.remove("tagify--mark")},2e3),!0)},isTagBlacklisted:function(t){return t=t.split(" "),this.settings.blacklist.filter(function(e){return-1!=t.indexOf(e)}).length},isTagWhitelisted:function(t){return-1!=this.settings.whitelist.indexOf(t)},addTag:function(t){var e=this;return this.DOM.input.removeAttribute("style"),(t=t.trim())?t.split(this.settings.delimiters).filter(function(t){return!!t}).map(function(t){if(t=t.trim(),e.settings.pattern&&!e.settings.pattern.test(t))return!1;var i,n,s=document.createElement("tag"),a=e.markTagByValue(t),r=e.value.length>=e.settings.maxTags;return!(a&&(e.trigger("duplicate",t),!e.settings.duplicates))&&((i=!e.isTagBlacklisted(t)&&(!e.settings.enforeWhitelist||e.isTagWhitelisted(t))&&!r)||(s.classList.add("tagify--notAllowed"),setTimeout(function(){e.removeTag(e.getNodeIndex(s),!0)},1e3),r?n="maxTagsExceed":e.isTagBlacklisted(t)?n="blacklisted":e.settings.enforeWhitelist&&!e.isTagWhitelisted(t)&&(n="notWhitelisted"),e.trigger(n,{value:t,index:e.value.length})),s.innerHTML="<x></x><div><span title='"+t+"'>"+t+" </span></div>",e.DOM.scope.insertBefore(s,e.DOM.input.parentNode),i&&(e.value.push(t),e.update(),e.trigger("add",{value:t,index:e.value.length})),s)}).filter(function(t){return t}):[]},removeTag:function(t,e){var i=this.DOM.scope.children[t];i&&(i.style.width=parseFloat(window.getComputedStyle(i).width)+"px",document.body.clientTop,i.classList.add("tagify--hide"),setTimeout(function(){i.parentNode.removeChild(i)},400),e||(this.value.splice(t,1),this.update(),this.trigger("remove",{value:i.textContent.trim(),index:t})))},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=this.value.join(",")}}}(jQuery);
"use strict";!function(t){function e(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"),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.id=Math.random().toString(36).substr(2,9),this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.events.customBinding.call(this),this.events.binding.call(this)}t.fn.tagify=function(){var i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.each(function(){var n,s=t(this);if(s.data("tagify"))return this;i.isJQueryPlugin=!0,n=new e(s[0],i),s.data("tagify",n)})},e.prototype={isIE:window.document.documentMode,DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10}},customEventsList:["add","remove","duplicate","maxTagsExceed","blacklisted","notWhitelisted"],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=t.value,i='\n <tags class="tagify '+t.className+" "+(this.settings.readonly?"readonly":"")+'">\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify--input"></div>\n </tags>';this.DOM.originalInput=t,this.DOM.scope=this.parseHTML(i),this.DOM.input=this.DOM.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(this.DOM.scope,t),this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),e&&this.addTags(e).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},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);return t===Object(t)&&"[object Array]"!=e&&"[object Function]"!=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(e){var i=document.createTextNode("");this.off=function(t,e){return e&&i.removeEventListener.call(i,t,e),this},this.on=function(t,e){return e&&i.addEventListener.call(i,t,e),this},this.trigger=function(n,s){var o;if(n)if(e.settings.isJQueryPlugin)t(e.DOM.originalInput).triggerHandler(n,[s]);else{try{o=new CustomEvent(n,{detail:s})}catch(t){console.warn(t)}i.dispatchEvent(o)}}},events:{customBinding:function(){var t=this;this.customEventsList.forEach(function(e){t.on(e,t.settings.callbacks[e])})},binding:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],i=this.events.callbacks,n=this.listeners.main=this.listeners.main||{paste:["input",i.onPaste.bind(this)],focus:["input",i.onFocusBlur.bind(this)],blur:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)]},s=e?"addEventListener":"removeEventListener";for(var o in n)this.DOM[n[o][0]][s](o,n[o][1]);e&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",i[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&t(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type||("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?(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=t.target.textContent.trim(),i=e.length>=this.settings.dropdown.enabled;this.input.value!=e&&(this.input.value=e,this.input.normalize.call(this),this.input.autocomplete.suggest.call(this,""),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown[i?"show":"hide"].call(this,e))},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)}}},input:{value:"",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";this.input.value=this.DOM.input.innerHTML=t,t.length<2&&this.input.autocomplete.suggest.call(this,"")},normalize:function(){for(;this.DOM.input.firstElementChild;)this.DOM.input.removeChild(this.DOM.input.firstElementChild)},autocomplete:{suggest:function(t){t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(){var t=this.DOM.input.getAttribute("data-suggest");t&&this.addTags(this.input.value+t).length&&(this.input.set.call(this),this.dropdown.hide.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(t){return this.value.some(function(e){return t.toLowerCase()===e.value.toLowerCase()})},markTagByValue:function(t,e){var i,n;if(!e)for(n=(i=this.DOM.scope.querySelectorAll("tag")).length;n--;)i[n].value.toLowerCase().includes(t.toLowerCase())&&(e=i[n]);return!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},2e3),!0)},isTagBlacklisted:function(t){return t=t.split(" "),this.settings.blacklist.filter(function(e){return-1!=t.indexOf(e)}).length},isTagWhitelisted:function(t){return this.settings.whitelist.some(function(e){if((e.value?e.value:e).toLowerCase()===t.toLowerCase())return!0})},addTags:function(t,e){function i(t){var e,i=t.value.trim(),n=this.value.length>=this.settings.maxTags;return i?this.settings.pattern&&!this.settings.pattern.test(i)?"pattern":this.isTagDuplicate(i)&&(this.trigger("duplicate",i),!this.settings.duplicates)?"duplicate":!!(!this.isTagBlacklisted(i)&&(!this.settings.enforceWhitelist||this.isTagWhitelisted(i))&&!n)||(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",n?e="maxTagsExceed":this.isTagBlacklisted(i)?e="blacklisted":this.settings.enforceWhitelist&&!this.isTagWhitelisted(i)&&(e="notWhitelisted"),this.trigger(e,{value:i,index:this.value.length}),"notAllowed"):"empty"}function n(t){this.DOM.scope.insertBefore(t,this.DOM.input)}var s=this,o=[];return this.DOM.input.removeAttribute("style"),(t=function(t){var e=this.settings.whitelist[0]instanceof Object,i=t instanceof Array&&"value"in t[0],n=t;if(i)return n;if(!i&&"string"==typeof t&&e){var s=this.settings.whitelist.filter(function(e){return e.value.toLowerCase()==t.toLowerCase()});s[0]&&(i=!0,n=s)}if(!i){if(!(t=t.trim()))return[];n=t.split(this.settings.delimiters).map(function(t){return{value:t.trim()}})}return n.filter(function(t){return t})}.call(this,t)).forEach(function(t){var e=i.call(s,t);if(!0===e||"notAllowed"==e){var a=s.createTagElem(t);n.call(s,a),"notAllowed"==e?setTimeout(function(){s.removeTag(a,!0)},1e3):(s.value.push(t),s.update(),s.trigger("add",s.extend({},{index:s.value.length,tag:a},t)),o.push(a))}}),t.length&&e&&this.input.set.call(this),o},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag>\n <x></x><div><span title='"+i+"'>"+i+"</span></div>\n </tag>";return e=this.parseHTML(n),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,t),e},removeTag:function(t,e){var i,n=this.getNodeIndex(t);t&&(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(function(){t.parentNode.removeChild(t)},400),e||(i=this.value.splice(n,1)[0],this.update(),this.trigger("remove",this.extend({},{index:n,tag:t},i))))},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 t=this.value.map(function(t){return t.value}).join(",");this.DOM.originalInput.value=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=this.dropdown.filterListItems.call(this,t),i=this.dropdown.createListHTML(e);e.length&&this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,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=!(arguments.length>0&&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]("click",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?"previousElementSibling":"nextElementSibling"]:this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0],this.dropdown.highlightOption.call(this,e);break;case"Escape":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)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){t.target.className.includes("tagify__dropdown__item")&&(this.input.set.call(this),this.addTags(t.target.textContent)),this.dropdown.hide.call(this)}}},highlightOption:function(t){if(t){var e="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){t.classList.remove(e)}),t.classList.add(e)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,o=0;o<n.length&&(e=n[o]instanceof Object?n[o]:{value:n[o]},0==e.value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&!this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);o++);return i},createListHTML:function(t){function e(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}return t.map(function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+e(t)+">"+t.value+"</div>"}).join("")}}}}(jQuery);
/**
* Tagify (v 1.2.1)- tags input component
* Tagify (v 1.3.1)- tags input component
* By Yair Even-Or (2016)

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

}(this, function() {
function Tagify( input, settings ){
'use strict';
function Tagify(input, settings) {
// protection
if( !input ){
console.warn('Tagify: ', 'invalid input element ', input)
if (!input) {
console.warn('Tagify: ', 'invalid input element ', input);
return this;
}
this.settings = this.extend({}, settings, this.DEFAULTS);
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){}
if (isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false
if( settings && settings.delimiters ){
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("[" + settings.delimiters + "]");
} catch(e){}
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g");
} catch (e) {}
}
this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it)
this.id = Math.random().toString(36).substr(2, 9), // almost-random ID (because, fuck it)
this.value = []; // An array holding all the (currently used) tags
// 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.extend(this, new this.EventDispatcher(this));
this.build(input);
this.events();
this.events.customBinding.call(this);
this.events.binding.call(this);
}
Tagify.prototype = {
DEFAULTS : {
delimiters : ",", // [regex] split tags by any of these delimiters
pattern : "", // pattern to validate input by
callbacks : {}, // exposed callbacks object to be triggered on certain events
duplicates : false, // flag - allow tuplicate tags
enforeWhitelist : false, // flag - should ONLY use tags allowed in whitelist
autocomplete : true, // flag - show native suggeestions list as you type
whitelist : [], // is this list has any items, then only allow tags from this list
blacklist : [], // a list of non-allowed tags
maxTags : Infinity, // maximum number of tags
suggestionsMinChars : 2 // minimum characters to input to see sugegstions list
isIE: window.document.documentMode,
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: [], // is this list has any items, then only allow tags from this list
blacklist: [], // a list of non-allowed tags
enforceWhitelist: false, // flag - should ONLY use tags allowed in whitelist
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
}
},
customEventsList: ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'],
/**
* 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);
var p = document.createElement('p');
p.appendChild(text);
return p.innerHTML;
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build : function( input ){
build: function build(input) {
var that = this,
value = input.value,
inputHTML = '<div><input list="tagifySuggestions'+ this.id +'" class="placeholder"/><span>'+ input.placeholder +'</span></div>';
template = '\n <tags class="tagify ' + input.className + ' ' + (this.settings.readonly ? 'readonly' : '') + '">\n <div contenteditable data-placeholder="' + input.placeholder + '" class="tagify--input"></div>\n </tags>';
this.DOM.originalInput = input;
this.DOM.scope = document.createElement('tags');
this.DOM.scope.innerHTML = inputHTML;
this.DOM.input = this.DOM.scope.querySelector('input');
if( this.settings.readonly )
this.DOM.scope.classList.add('readonly')
this.DOM.scope = this.parseHTML(template);
this.DOM.input = this.DOM.scope.querySelector('[contenteditable]');
input.parentNode.insertBefore(this.DOM.scope, input);
this.DOM.scope.appendChild(input);
// if "autocomplete" flag on toggeled & "whitelist" has items, build suggestions list
if( this.settings.autocomplete && this.settings.whitelist.length )
this.DOM.datalist = this.buildDataList();
if (this.settings.dropdown.enabled && this.settings.whitelist.length) {
this.dropdown.init.call(this);
}
// if the original input already had any value (tags)
if( value )
this.addTag(value).forEach(function(tag){
tag && tag.classList.add('tagify--noAnim');
});
if (value) this.addTags(value).forEach(function (tag) {
tag && tag.classList.add('tagify--noAnim');
});
},
/**
* Reverts back any changes made by this component
*/
destroy : function(){
this.DOM.scope.parentNode.appendChild(this.DOM.originalInput);
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(o, o1, o2){
if( !(o instanceof Object) ) o = {};
extend: function extend(o, o1, o2) {
if (!(o instanceof Object)) o = {};
if( o2 ){
copy(o, o2)
copy(o, o1)
}
else
copy(o, o1)
copy(o, o1);
if (o2) copy(o, o2);
function copy(a,b){
function isObject(obj) {
var type = Object.prototype.toString.call(obj);
return obj === Object(obj) && type != '[object Array]' && type != '[object Function]';
};
function copy(a, b) {
// copy o2 to o
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
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];
}
}
}

@@ -121,6 +165,7 @@

/**
* A constructor for exposing events to the outside
*/
EventDispatcher : function(){
EventDispatcher: function EventDispatcher(instance) {
// Create a DOM EventTarget object

@@ -130,129 +175,130 @@ var target = document.createTextNode('');

// Pass EventTarget interface calls to DOM EventTarget object
this.off = target.removeEventListener.bind(target);
this.on = target.addEventListener.bind(target);
this.trigger = function(eventName, data){
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 (!eventName) return;
if( this.isJQueryPlugin )
$(this.DOM.originalInput).triggerHandler(eventName, [data])
else{
if (instance.settings.isJQueryPlugin) {
$(instance.DOM.originalInput).triggerHandler(eventName, [data]);
} else {
try {
e = new CustomEvent(eventName, {"detail":data});
e = new CustomEvent(eventName, { "detail": data });
} catch (err) {
console.warn(err);
}
catch(err){
e = document.createEvent("Event");
e.initEvent("toggle", false, false);
}
target.dispatchEvent(e);
}
}
};
},
/**
* DOM events listeners binding
*/
events : function(){
var that = this,
events = {
// event name / event callback / element to be listening to
paste : ['onPaste' , 'input'],
focus : ['onFocusBlur' , 'input'],
blur : ['onFocusBlur' , 'input'],
input : ['onInput' , 'input'],
keydown : ['onKeydown' , 'input'],
click : ['onClickScope' , 'scope']
},
customList = ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'];
events: {
// bind custom events which were passed in the settings
customBinding: function customBinding() {
var _this2 = this;
for( var e in events )
this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].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;
customList.forEach(function(name){
that.on(name, that.settings.callbacks[name])
})
var _CB = this.events.callbacks,
if( this.isJQueryPlugin )
$(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))
},
// setup callback references so events could be removed later
_CBR = this.listeners.main = this.listeners.main || {
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';
/**
* DOM events callbacks
*/
callbacks : {
onFocusBlur : function(e){
var text = e.target.value.trim();
if( e.type == "focus" )
e.target.className = 'input';
else if( e.type == "blur" && text ){
if( this.addTag(text).length )
e.target.value = '';
for (var eventName in _CBR) {
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
else{
e.target.className = 'input placeholder';
this.DOM.input.removeAttribute('style');
}
},
onKeydown : function(e){
var s = e.target.value,
that = 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( e.key == "Backspace" && (s == "" || s.charCodeAt(0) == 8203) ){
this.removeTag( this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)').length - 1 );
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this));
}
if( e.key == "Escape" ){
e.target.value = '';
e.target.blur();
}
if( e.key == "Enter" ){
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
if( this.addTag(s).length )
e.target.value = '';
return false;
}
else{
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
}
},
onInput : function(e){
var value = e.target.value,
lastChar = value[value.length - 1],
isDatalistInput = !this.noneDatalistInput && value.length > 1,
showSuggestions = value.length >= this.settings.suggestionsMinChars,
datalistInDOM;
e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';
/**
* DOM events callbacks
*/
callbacks: {
onFocusBlur: function onFocusBlur(e) {
var s = e.target.textContent.trim();
// if( value.indexOf(',') != -1 || isDatalistInput ){
if( value.slice().search(this.settings.delimiters) != -1 || isDatalistInput ){
if( this.addTag(value).length )
e.target.value = ''; // clear the input field's value
}
else if( this.settings.autocomplete && this.settings.whitelist.length ){
datalistInDOM = this.DOM.input.parentNode.contains( this.DOM.datalist );
// if sugegstions should be hidden
if( !showSuggestions && datalistInDOM )
this.DOM.input.parentNode.removeChild(this.DOM.datalist)
else if( showSuggestions && !datalistInDOM ){
this.DOM.input.parentNode.appendChild(this.DOM.datalist)
if (e.type == "focus") {
// e.target.classList.remove('placeholder');
} 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;
onPaste : function(e){
var that = this;
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
},
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') {
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 = e.target.textContent.trim(),
showSuggestions = value.length >= this.settings.dropdown.enabled;
onClickScope : function(e){
if( e.target.tagName == "TAGS" )
this.DOM.input.focus();
if( e.target.tagName == "X" ){
this.removeTag( this.getNodeIndex(e.target.parentNode) );
if (this.input.value == value) return;
// save the value on the input state object
this.input.value = value;
this.input.normalize.call(this);
this.input.autocomplete.suggest.call(this, ''); // cleanup any possible previous suggestion
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 && this.settings.whitelist.length) 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);
}
}

@@ -263,145 +309,300 @@ }

/**
* Build tags suggestions using HTML datalist
* @return {[type]} [description]
* input bridge for accessing & setting
* @type {Object}
*/
buildDataList : function(){
var OPTIONS = "",
i,
datalist = document.createElement('datalist');
input: {
value: '',
set: function set() {
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
datalist.id = 'tagifySuggestions' + this.id;
datalist.innerHTML = "<label> \
select from the list: \
<select> \
<option value=''></option> \
[OPTIONS] \
</select> \
</label>";
this.input.value = this.DOM.input.innerHTML = s;
for( i=this.settings.whitelist.length; i--; )
OPTIONS += "<option>"+ this.settings.whitelist[i] +"</option>";
if (s.length < 2) this.input.autocomplete.suggest.call(this, '');
},
datalist.innerHTML = datalist.innerHTML.replace('[OPTIONS]', OPTIONS); // inject the options string in the right place
// this.DOM.input.insertAdjacentHTML('afterend', datalist); // append the datalist HTML string in the Tags
// remove any child DOM elements that aren't of type TEXT (like <br>)
normalize: function normalize() {
while (this.DOM.input.firstElementChild) {
this.DOM.input.removeChild(this.DOM.input.firstElementChild);
}
},
return datalist;
/**
* suggest the rest of the input's value
* @param {String} s [description]
*/
autocomplete: {
suggest: function suggest(s) {
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest");
},
set: function set() {
var suggestion = this.DOM.input.getAttribute('data-suggest');
if (suggestion && this.addTags(this.input.value + suggestion).length) {
this.input.set.call(this);
this.dropdown.hide.call(this);
}
}
}
},
getNodeIndex : function( node ){
getNodeIndex: function getNodeIndex(node) {
var index = 0;
while( (node = node.previousSibling) )
if (node.nodeType != 3 || !/^\s*$/.test(node.data))
index++;
return index;
while (node = node.previousSibling) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++;
}return index;
},
/**
* Searches if any tags with a certain value exist and mark them
* @param {String / Number} value [description]
* @return {boolean} [found / not found]
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {boolean} [found / not found]
*/
markTagByValue : function(value){
var idx = this.value.filter(function(item){ return value.toLowerCase() === item.toLowerCase() })[0],
tag = this.DOM.scope.querySelectorAll('tag')[idx];
isTagDuplicate: function isTagDuplicate(s) {
return this.value.some(function (item) {
return s.toLowerCase() === item.value.toLowerCase();
});
},
if( tag ){
tag.classList.add('tagify--mark');
setTimeout(function(){ tag.classList.remove('tagify--mark') }, 2000);
return true;
/**
* 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;
if (!tagElm) {
tagsElms = this.DOM.scope.querySelectorAll('tag');
for (tagsElmsLen = tagsElms.length; tagsElmsLen--;) {
if (tagsElms[tagsElmsLen].value.toLowerCase().includes(value.toLowerCase())) tagElm = tagsElms[tagsElmsLen];
}
}
// check AGAIN if "tagElm" is defined
if (tagElm) {
tagElm.classList.add('tagify--mark');
setTimeout(function () {
tagElm.classList.remove('tagify--mark');
}, 2000);
return true;
} else {}
return false;
},
/**
* make sure the tag, or words in it, is not in the blacklist
*/
isTagBlacklisted : function(v){
isTagBlacklisted: function isTagBlacklisted(v) {
v = v.split(' ');
return this.settings.blacklist.filter(function(x){ return v.indexOf(x) != -1 }).length;
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(v){
return this.settings.whitelist.indexOf(v) != -1;
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;
});
},
/**
* add a "tag" element to the "tags" component
* @param {String} value [A string of a value or multiple values]
* @return {Array} Array of DOM elements
* @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)
*/
addTag : function( value ){
addTags: function addTags(tagsItems, clearInput) {
var that = this,
result;
tagElems = [];
this.DOM.input.removeAttribute('style');
value = value.trim();
/**
* 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]
*/
function 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
result = tagsItems; // the returned result
if( !value ) return [];
// no need to continue if "tagsItems" is an Array of Objects
if (isComplex) return result;
// go over each tag and add it (if there were multiple ones)
result = value.split(this.settings.delimiters).filter(function(v){ return !!v }).map(function(v){
v = v.trim();
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if (!isComplex && typeof tagsItems == "string" && whitelistWithProps) {
var matchObj = this.settings.whitelist.filter(function (item) {
return item.value.toLowerCase() == tagsItems.toLowerCase();
});
if( that.settings.pattern && !(that.settings.pattern.test(v)) )
return false;
if (matchObj[0]) {
isComplex = true;
result = matchObj; // set the Array (with the found Object) as the new value
}
}
var tagElm = document.createElement('tag'),
isDuplicate = that.markTagByValue(v),
tagAllowed,
tagNotAllowedEventName,
maxTagsExceed = that.value.length >= that.settings.maxTags;
// if the value is a "simple" String, ex: "aaa, bbb, ccc"
if (!isComplex) {
tagsItems = tagsItems.trim();
if (!tagsItems) return [];
if( isDuplicate ){
that.trigger('duplicate', v);
if( !that.settings.duplicates ){
return false;
// go over each tag and add it (if there were multiple ones)
result = tagsItems.split(this.settings.delimiters).map(function (v) {
return { value: v.trim() };
});
}
return result.filter(function (n) {
return n;
}); // cleanup the array from "undefined", "false" or empty items;
}
/**
* validate a tag object BEFORE the actual tag will be created & appeneded
* @param {Object} tagData [{"value":"text", "class":whatever", ...}]
* @return {Boolean/String} ["true" if validation has passed, String or "false" for any type of error]
*/
function validateTag(tagData) {
var value = tagData.value.trim(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
tagAllowed;
// check for empty value
if (!value) return "empty";
// check if pattern should be used and if so, use it to test the value
if (this.settings.pattern && !this.settings.pattern.test(value)) return "pattern";
// check if the tag already exists
if (this.isTagDuplicate(value)) {
this.trigger('duplicate', value);
if (!this.settings.duplicates) {
// this.markTagByValue(value, tagElm)
return "duplicate";
}
}
tagAllowed = !that.isTagBlacklisted(v) && (!that.settings.enforeWhitelist || that.isTagWhitelisted(v)) && !maxTagsExceed;
// check if the tag is allowed by the rules set
tagAllowed = !this.isTagBlacklisted(value) && (!this.settings.enforceWhitelist || this.isTagWhitelisted(value)) && !maxTagsExceed;
// check against blacklist & whitelist (if enforced)
if( !tagAllowed ){
tagElm.classList.add('tagify--notAllowed');
setTimeout(function(){ that.removeTag(that.getNodeIndex(tagElm), true) }, 1000);
// Check against blacklist & whitelist (if enforced)
if (!tagAllowed) {
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
// broadcast why the tag was not allowed
if( maxTagsExceed ) tagNotAllowedEventName = 'maxTagsExceed';
else if( that.isTagBlacklisted(v) ) tagNotAllowedEventName = 'blacklisted';
else if( that.settings.enforeWhitelist && !that.isTagWhitelisted(v) ) tagNotAllowedEventName = 'notWhitelisted';
if (maxTagsExceed) eventName__error = 'maxTagsExceed';else if (this.isTagBlacklisted(value)) eventName__error = 'blacklisted';else if (this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) eventName__error = 'notWhitelisted';
that.trigger(tagNotAllowedEventName, {value:v, index:that.value.length});
this.trigger(eventName__error, { value: value, index: this.value.length });
return "notAllowed";
}
// the space below is important - http://stackoverflow.com/a/19668740/104380
tagElm.innerHTML = "<x></x><div><span title='"+ v +"'>"+ v +" </span></div>";
that.DOM.scope.insertBefore(tagElm, that.DOM.input.parentNode);
return true;
}
if( tagAllowed ){
that.value.push(v);
that.update();
that.trigger('add', {value:v, index:that.value.length});
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
function appendTag(tagElm) {
this.DOM.scope.insertBefore(tagElm, this.DOM.input);
}
//////////////////////
tagsItems = normalizeTags.call(this, tagsItems);
tagsItems.forEach(function (tagData) {
var isTagValidated = validateTag.call(that, tagData);
if (isTagValidated === true || isTagValidated == "notAllowed") {
// create the tag element
var tagElm = that.createTagElem(tagData);
// add the tag to the component's DOM
appendTag.call(that, tagElm);
// remove the tag "slowly"
if (isTagValidated == "notAllowed") {
setTimeout(function () {
that.removeTag(tagElm, true);
}, 1000);
} else {
// update state
that.value.push(tagData);
that.update();
that.trigger('add', that.extend({}, { index: that.value.length, tag: tagElm }, tagData));
tagElems.push(tagElm);
}
}
return tagElm;
});
return result.filter(function(n){ return n });
if (tagsItems.length && clearInput) {
this.input.set.call(this);
}
return tagElems;
},
/**
* 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,
escapedValue = this.escapeHtml(tagData.value),
template = '<tag>\n <x></x><div><span title=\'' + escapedValue + '\'>' + escapedValue + '</span></div>\n </tag>';
// 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);
// add any attribuets, if exists
addTagAttrs(tagElm, tagData);
return tagElm;
},
/**
* Removes a tag
* @param {Number} idx [tag index to be removed]
* @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 {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]
*/
removeTag : function( idx, silent ){
var tagElm = this.DOM.scope.children[idx];
if( !tagElm) return;
removeTag: function removeTag(tagElm, silent) {
var tagData,
tagIdx = this.getNodeIndex(tagElm);
if (!tagElm) return;
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px';

@@ -412,30 +613,236 @@ document.body.clientTop; // force repaint for the width to take affect before the "hide" class below

// manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(function(){
setTimeout(function () {
tagElm.parentNode.removeChild(tagElm);
}, 400);
if( !silent ){
this.value.splice(idx, 1); // remove the tag from the data object
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', {value:tagElm.textContent.trim(), index:idx});
this.trigger('remove', this.extend({}, { index: tagIdx, tag: tagElm }, tagData));
}
},
removeAllTags : function(){
removeAllTags: function removeAllTags() {
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function(elm){
elm.parentNode.removeChild(elm);
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) {
return elm.parentNode.removeChild(elm);
});
},
/**
* update the origianl (hidden) input field's value
*/
update : function(){
this.DOM.originalInput.value = this.value.join(',');
update: function update() {
var tagsAsString = this.value.map(function (v) {
return v.value;
}).join(',');
this.DOM.originalInput.value = tagsAsString;
},
/**
* 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 = this.dropdown.filterListItems.call(this, value),
listHTML = this.dropdown.createListHTML(listItems);
if (listItems.length && this.settings.autoComplete) this.input.autocomplete.suggest.call(this, listItems[0].value);
if (!listHTML || listItems.length < 2) {
this.dropdown.hide.call(this);
return;
}
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 (!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;
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
},
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;
// 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';
window[action]('resize', _CBR.position);
window[action]('keydown', _CBR.onKeyDown);
window[action]('click', _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 = "";
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' ? "previousElementSibling" : "nextElementSibling"];
// if no element was found, loop
else selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm);
break;
case 'Escape':
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);
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) {
if (e.target.className.includes('tagify__dropdown__item')) {
this.input.set.call(this);
this.addTags(e.target.textContent);
}
// clicked outside the dropdown, so just close it
this.dropdown.hide.call(this);
}
}
},
highlightOption: function highlightOption(elm) {
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) {
activeElm.classList.remove(className);
});
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
},
/**
* 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) && 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) {
function getItem(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 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(){function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},e,this.DEFAULTS),this.settings.readonly=t.hasAttribute("readonly"),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(e&&e.delimiters)try{this.settings.delimiters=new RegExp("["+e.delimiters+"]")}catch(t){}this.id=Math.random().toString(36).substr(2,9),this.value=[],this.DOM={},this.extend(this,new this.EventDispatcher),this.build(t),this.events()}return t.prototype={DEFAULTS:{delimiters:",",pattern:"",callbacks:{},duplicates:!1,enforeWhitelist:!1,autocomplete:!0,whitelist:[],blacklist:[],maxTags:1/0,suggestionsMinChars:2},build:function(t){var e=t.value,i='<div><input list="tagifySuggestions'+this.id+'" class="placeholder"/><span>'+t.placeholder+"</span></div>";this.DOM.originalInput=t,this.DOM.scope=document.createElement("tags"),this.DOM.scope.innerHTML=i,this.DOM.input=this.DOM.scope.querySelector("input"),this.settings.readonly&&this.DOM.scope.classList.add("readonly"),t.parentNode.insertBefore(this.DOM.scope,t),this.DOM.scope.appendChild(t),this.settings.autocomplete&&this.settings.whitelist.length&&(this.DOM.datalist=this.buildDataList()),e&&this.addTag(e).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},destroy:function(){this.DOM.scope.parentNode.appendChild(this.DOM.originalInput),this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}return t instanceof Object||(t={}),i?(n(t,i),n(t,e)):n(t,e),t},EventDispatcher:function(){var t=document.createTextNode("");this.off=t.removeEventListener.bind(t),this.on=t.addEventListener.bind(t),this.trigger=function(e,i){var n;if(e)if(this.isJQueryPlugin)$(this.DOM.originalInput).triggerHandler(e,[i]);else{try{n=new CustomEvent(e,{detail:i})}catch(t){(n=document.createEvent("Event")).initEvent("toggle",!1,!1)}t.dispatchEvent(n)}}},events:function(){var t=this,e={paste:["onPaste","input"],focus:["onFocusBlur","input"],blur:["onFocusBlur","input"],input:["onInput","input"],keydown:["onKeydown","input"],click:["onClickScope","scope"]},i=["add","remove","duplicate","maxTagsExceed","blacklisted","notWhitelisted"];for(var n in e)this.DOM[e[n][1]].addEventListener(n,this.callbacks[e[n][0]].bind(this));i.forEach(function(e){t.on(e,t.settings.callbacks[e])}),this.isJQueryPlugin&&$(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))},callbacks:{onFocusBlur:function(t){var e=t.target.value.trim();"focus"==t.type?t.target.className="input":"blur"==t.type&&e?this.addTag(e).length&&(t.target.value=""):(t.target.className="input placeholder",this.DOM.input.removeAttribute("style"))},onKeydown:function(t){var e=t.target.value,i=this;if("Backspace"!=t.key||""!=e&&8203!=e.charCodeAt(0)||this.removeTag(this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)").length-1),"Escape"==t.key&&(t.target.value="",t.target.blur()),"Enter"==t.key)return t.preventDefault(),this.addTag(e).length&&(t.target.value=""),!1;this.noneDatalistInput&&clearTimeout(this.noneDatalistInput),this.noneDatalistInput=setTimeout(function(){i.noneDatalistInput=null},50)},onInput:function(t){var e,i=t.target.value,n=(i[i.length-1],!this.noneDatalistInput&&i.length>1),s=i.length>=this.settings.suggestionsMinChars;t.target.style.width=7*(t.target.value.length+1)+"px",-1!=i.slice().search(this.settings.delimiters)||n?this.addTag(i).length&&(t.target.value=""):this.settings.autocomplete&&this.settings.whitelist.length&&(e=this.DOM.input.parentNode.contains(this.DOM.datalist),!s&&e?this.DOM.input.parentNode.removeChild(this.DOM.datalist):s&&!e&&this.DOM.input.parentNode.appendChild(this.DOM.datalist))},onPaste:function(t){var e=this;this.noneDatalistInput&&clearTimeout(this.noneDatalistInput),this.noneDatalistInput=setTimeout(function(){e.noneDatalistInput=null},50)},onClickScope:function(t){"TAGS"==t.target.tagName&&this.DOM.input.focus(),"X"==t.target.tagName&&this.removeTag(this.getNodeIndex(t.target.parentNode))}},buildDataList:function(){var t,e="",i=document.createElement("datalist");for(i.id="tagifySuggestions"+this.id,i.innerHTML="<label> select from the list: <select> <option value=''></option> [OPTIONS] </select> </label>",t=this.settings.whitelist.length;t--;)e+="<option>"+this.settings.whitelist[t]+"</option>";return i.innerHTML=i.innerHTML.replace("[OPTIONS]",e),i},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},markTagByValue:function(t){var e=this.value.filter(function(e){return t.toLowerCase()===e.toLowerCase()})[0],i=this.DOM.scope.querySelectorAll("tag")[e];return!!i&&(i.classList.add("tagify--mark"),setTimeout(function(){i.classList.remove("tagify--mark")},2e3),!0)},isTagBlacklisted:function(t){return t=t.split(" "),this.settings.blacklist.filter(function(e){return-1!=t.indexOf(e)}).length},isTagWhitelisted:function(t){return-1!=this.settings.whitelist.indexOf(t)},addTag:function(t){var e=this;return this.DOM.input.removeAttribute("style"),(t=t.trim())?t.split(this.settings.delimiters).filter(function(t){return!!t}).map(function(t){if(t=t.trim(),e.settings.pattern&&!e.settings.pattern.test(t))return!1;var i,n,s=document.createElement("tag"),a=e.markTagByValue(t),o=e.value.length>=e.settings.maxTags;return!(a&&(e.trigger("duplicate",t),!e.settings.duplicates))&&((i=!e.isTagBlacklisted(t)&&(!e.settings.enforeWhitelist||e.isTagWhitelisted(t))&&!o)||(s.classList.add("tagify--notAllowed"),setTimeout(function(){e.removeTag(e.getNodeIndex(s),!0)},1e3),o?n="maxTagsExceed":e.isTagBlacklisted(t)?n="blacklisted":e.settings.enforeWhitelist&&!e.isTagWhitelisted(t)&&(n="notWhitelisted"),e.trigger(n,{value:t,index:e.value.length})),s.innerHTML="<x></x><div><span title='"+t+"'>"+t+" </span></div>",e.DOM.scope.insertBefore(s,e.DOM.input.parentNode),i&&(e.value.push(t),e.update(),e.trigger("add",{value:t,index:e.value.length})),s)}).filter(function(t){return t}):[]},removeTag:function(t,e){var i=this.DOM.scope.children[t];i&&(i.style.width=parseFloat(window.getComputedStyle(i).width)+"px",document.body.clientTop,i.classList.add("tagify--hide"),setTimeout(function(){i.parentNode.removeChild(i)},400),e||(this.value.splice(t,1),this.update(),this.trigger("remove",{value:i.textContent.trim(),index:t})))},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=this.value.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"),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.id=Math.random().toString(36).substr(2,9),this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.events.customBinding.call(this),this.events.binding.call(this)}return t.prototype={isIE:window.document.documentMode,DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10}},customEventsList:["add","remove","duplicate","maxTagsExceed","blacklisted","notWhitelisted"],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=t.value,i='\n <tags class="tagify '+t.className+" "+(this.settings.readonly?"readonly":"")+'">\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify--input"></div>\n </tags>';this.DOM.originalInput=t,this.DOM.scope=this.parseHTML(i),this.DOM.input=this.DOM.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(this.DOM.scope,t),this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),e&&this.addTags(e).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},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);return t===Object(t)&&"[object Array]"!=e&&"[object Function]"!=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(t){var e=document.createTextNode("");this.off=function(t,i){return i&&e.removeEventListener.call(e,t,i),this},this.on=function(t,i){return i&&e.addEventListener.call(e,t,i),this},this.trigger=function(i,n){var s;if(i)if(t.settings.isJQueryPlugin)$(t.DOM.originalInput).triggerHandler(i,[n]);else{try{s=new CustomEvent(i,{detail:n})}catch(t){console.warn(t)}e.dispatchEvent(s)}}},events:{customBinding:function(){var t=this;this.customEventsList.forEach(function(e){t.on(e,t.settings.callbacks[e])})},binding:function(){var t=!(arguments.length>0&&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||("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?(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=t.target.textContent.trim(),i=e.length>=this.settings.dropdown.enabled;this.input.value!=e&&(this.input.value=e,this.input.normalize.call(this),this.input.autocomplete.suggest.call(this,""),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown[i?"show":"hide"].call(this,e))},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)}}},input:{value:"",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";this.input.value=this.DOM.input.innerHTML=t,t.length<2&&this.input.autocomplete.suggest.call(this,"")},normalize:function(){for(;this.DOM.input.firstElementChild;)this.DOM.input.removeChild(this.DOM.input.firstElementChild)},autocomplete:{suggest:function(t){t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(){var t=this.DOM.input.getAttribute("data-suggest");t&&this.addTags(this.input.value+t).length&&(this.input.set.call(this),this.dropdown.hide.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(t){return this.value.some(function(e){return t.toLowerCase()===e.value.toLowerCase()})},markTagByValue:function(t,e){var i,n;if(!e)for(n=(i=this.DOM.scope.querySelectorAll("tag")).length;n--;)i[n].value.toLowerCase().includes(t.toLowerCase())&&(e=i[n]);return!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},2e3),!0)},isTagBlacklisted:function(t){return t=t.split(" "),this.settings.blacklist.filter(function(e){return-1!=t.indexOf(e)}).length},isTagWhitelisted:function(t){return this.settings.whitelist.some(function(e){if((e.value?e.value:e).toLowerCase()===t.toLowerCase())return!0})},addTags:function(t,e){function i(t){var e,i=t.value.trim(),n=this.value.length>=this.settings.maxTags;return i?this.settings.pattern&&!this.settings.pattern.test(i)?"pattern":this.isTagDuplicate(i)&&(this.trigger("duplicate",i),!this.settings.duplicates)?"duplicate":!!(!this.isTagBlacklisted(i)&&(!this.settings.enforceWhitelist||this.isTagWhitelisted(i))&&!n)||(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",n?e="maxTagsExceed":this.isTagBlacklisted(i)?e="blacklisted":this.settings.enforceWhitelist&&!this.isTagWhitelisted(i)&&(e="notWhitelisted"),this.trigger(e,{value:i,index:this.value.length}),"notAllowed"):"empty"}function n(t){this.DOM.scope.insertBefore(t,this.DOM.input)}var s=this,o=[];return this.DOM.input.removeAttribute("style"),(t=function(t){var e=this.settings.whitelist[0]instanceof Object,i=t instanceof Array&&"value"in t[0],n=t;if(i)return n;if(!i&&"string"==typeof t&&e){var s=this.settings.whitelist.filter(function(e){return e.value.toLowerCase()==t.toLowerCase()});s[0]&&(i=!0,n=s)}if(!i){if(!(t=t.trim()))return[];n=t.split(this.settings.delimiters).map(function(t){return{value:t.trim()}})}return n.filter(function(t){return t})}.call(this,t)).forEach(function(t){var e=i.call(s,t);if(!0===e||"notAllowed"==e){var a=s.createTagElem(t);n.call(s,a),"notAllowed"==e?setTimeout(function(){s.removeTag(a,!0)},1e3):(s.value.push(t),s.update(),s.trigger("add",s.extend({},{index:s.value.length,tag:a},t)),o.push(a))}}),t.length&&e&&this.input.set.call(this),o},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag>\n <x></x><div><span title='"+i+"'>"+i+"</span></div>\n </tag>";return e=this.parseHTML(n),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,t),e},removeTag:function(t,e){var i,n=this.getNodeIndex(t);t&&(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(function(){t.parentNode.removeChild(t)},400),e||(i=this.value.splice(n,1)[0],this.update(),this.trigger("remove",this.extend({},{index:n,tag:t},i))))},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 t=this.value.map(function(t){return t.value}).join(",");this.DOM.originalInput.value=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=this.dropdown.filterListItems.call(this,t),i=this.dropdown.createListHTML(e);e.length&&this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,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=!(arguments.length>0&&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]("click",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?"previousElementSibling":"nextElementSibling"]:this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0],this.dropdown.highlightOption.call(this,e);break;case"Escape":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)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){t.target.className.includes("tagify__dropdown__item")&&(this.input.set.call(this),this.addTags(t.target.textContent)),this.dropdown.hide.call(this)}}},highlightOption:function(t){if(t){var e="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){t.classList.remove(e)}),t.classList.add(e)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,o=0;o<n.length&&(e=n[o]instanceof Object?n[o]:{value:n[o]},0==e.value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&!this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);o++);return i},createListHTML:function(t){function e(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}return t.map(function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+e(t)+">"+t.value+"</div>"}).join("")}}},t});

@@ -1,28 +0,6 @@

var gulp = require('gulp'),
gutil = require('gulp-util'),
watch = require('gulp-watch'),
cache = require('gulp-cached'),
gulp_if = require('gulp-if'),
runSequence = require('run-sequence'),
var gulp = require('gulp'),
$ = require( "gulp-load-plugins" )({ pattern:['*', 'gulp-'] }),
pkg = require('./package.json');
sourcemaps = require('gulp-sourcemaps'),
//debug = require('gulp-debug'),
sass = require('gulp-sass'),
csso = require('gulp-csso'),
combineMq = require('gulp-combine-mq'),
cssGlobbing = require('gulp-css-globbing'),
autoprefixer = require('gulp-autoprefixer'),
umd = require('gulp-umd'),
uglify = require('gulp-uglify'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
eslint = require('gulp-eslint'),
replace = require('gulp-replace'),
insert = require('gulp-insert'),
beep = require('beepbeep'),
pkg = require('./package.json');
var uglifyOptions = {

@@ -105,14 +83,14 @@ compress: {

// just a jQuery wrapper for the vanilla version of this component
$.fn.tagify = function(settings){
var $input = this,
tagify;
$.fn.tagify = function(settings = {}){
return this.each(function() {
var $input = $(this),
tagify;
if( $input.data("tagify") ) // don't continue if already "tagified"
return this;
if( $input.data("tagify") ) // don't continue if already "tagified"
return this;
tagify = new Tagify(this[0], settings);
tagify.isJQueryPlugin = true;
$input.data("tagify", tagify);
return this;
settings.isJQueryPlugin = true;
tagify = new Tagify($input[0], settings);
$input.data("tagify", tagify);
});
}

@@ -132,10 +110,11 @@

return gulp.src('src/*.scss')
.pipe(cssGlobbing({
.pipe($.cssGlobbing({
extensions: '.scss'
}))
.pipe(
sass().on('error', sass.logError)
$.sass().on('error', $.sass.logError)
)
.pipe(combineMq()) // combine media queries
.pipe( autoprefixer({ browsers:['last 7 versions'] }) )
// .pipe($.combineMq()) // combine media queries
.pipe($.autoprefixer({ browsers:['last 7 versions'] }) )
.pipe($.cleanCss())
.pipe(gulp.dest('./dist'))

@@ -146,12 +125,10 @@ });

gulp.task('build_js', () => {
gulp.task('build_js', ['lint_js'], () => {
var jsStream = gulp.src('src/tagify.js');
lint(jsStream);
return gulp.src('src/tagify.js')
.pipe(umd())
.pipe(insert.prepend(banner))
.pipe(gulp.dest('./dist/'))
return jsStream
.pipe( $.babel({presets: ['env']}) )
.pipe( $.umd() )
.pipe( $.insert.prepend(banner) )
.pipe( gulp.dest('./dist/') )
});

@@ -163,4 +140,5 @@

return gulp.src('src/tagify.js')
.pipe(insert.wrap(banner + jQueryPluginWrap[0], jQueryPluginWrap[1]))
.pipe(rename('jQuery.tagify.js'))
.pipe($.insert.wrap(banner + jQueryPluginWrap[0], jQueryPluginWrap[1]))
.pipe( $.babel({presets: ['env']}) )
.pipe($.rename('jQuery.tagify.js'))
.pipe(gulp.dest('./dist/'))

@@ -170,14 +148,13 @@ });

gulp.task('minify', () => {
gulp.src('dist/tagify.js')
.pipe(uglify())
.pipe($.uglify())
.on('error', handleError)
.pipe(rename('tagify.min.js'))
.pipe($.rename('tagify.min.js'))
.pipe(gulp.dest('./dist/'))
return gulp.src('dist/jQuery.tagify.js')
.pipe(uglify())
.pipe($.uglify())
.on('error', handleError)
.pipe(rename('jQuery.tagify.min.js'))
.pipe($.rename('jQuery.tagify.min.js'))
.pipe(gulp.dest('./dist/'))

@@ -187,20 +164,23 @@ });

function handleError(err) {
gutil.log( err.toString() );
$.util.log( err.toString() );
this.emit('end');
}
function lint( stream ){
return stream
/**
* lints the javscript source code using "eslint"
*/
gulp.task('lint_js', () => {
return gulp.src('src/tagify.js')
// eslint() attaches the lint output to the eslint property
// of the file object so it can be used by other modules.
.pipe(cache('linting'))
.pipe(eslint(eslint_settings))
// eslint.format() outputs the lint results to the console.
.pipe($.cached('linting'))
.pipe($.eslint(eslint_settings))
// $.eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
.pipe(eslint.format())
.pipe($.eslint.format())
// To have the process exit with an error code (1) on
// lint error, return the stream and pipe to failAfterError last.
.pipe(eslint.failAfterError())
.on('error', beep);
}
.pipe($.eslint.failAfterError())
.on('error', $.beepbeep)
});

@@ -211,3 +191,3 @@

gulp.watch('./src/*.scss', ['scss']);
gulp.watch('./src/tagify.js').on('change', ()=>{ runSequence('build_js', 'build_jquery_version', 'minify') });
gulp.watch('./src/tagify.js').on('change', ()=>{ $.runSequence('build_js', 'build_jquery_version', 'minify') });
});

@@ -217,3 +197,3 @@

gulp.task('default', ( done ) => {
runSequence(['build_js', 'scss'], 'build_jquery_version', 'minify', 'watch', done);
$.runSequence(['build_js', 'scss'], 'build_jquery_version', 'minify', 'watch', done);
});
{
"name" : "@yaireo/tagify",
"version" : "1.2.2",
"homepage" : "https://github.com/yairEO/tagify",
"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.",
"_npmUser" : {
"name" : "vsync",
"email" : "vsync.design@gmail.com"
"name": "@yaireo/tagify",
"version": "2.0.0",
"homepage": "https://github.com/yairEO/tagify",
"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.",
"_npmUser": {
"name": "vsync",
"email": "vsync.design@gmail.com"
},
"author": {
"name" : "Yair Even-Or",
"email" : "vsync.design@gmail.com"
"name": "Yair Even-Or",
"email": "vsync.design@gmail.com"
},
"main" : "tagify.js",
"repository" : {
"type" : "git",
"url" : "git+https://github.com/yairEO/tagify.git"
"main": "tagify.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yairEO/tagify.git"
},

@@ -23,25 +23,27 @@ "bugs": {

"devDependencies": {
"beepbeep" : "^1.2.1",
"gulp" : "^3.9.1",
"gulp-autoprefixer" : "4.0.0",
"gulp-cached" : "^1.1.1",
"gulp-combine-mq" : "^0.4.0",
"gulp-concat" : "^2.6.1",
"gulp-css-globbing" : "^0.1.9",
"gulp-csso" : "^3.0.0",
"gulp-eslint" : "^3.0.1",
"gulp-if" : "^2.0.2",
"gulp-insert" : "^0.5.0",
"gulp-minify-css" : "^1.2.4",
"gulp-rename" : "~1.2.2",
"gulp-replace" : "^0.5.4",
"gulp-sass" : "^3.1.0",
"gulp-sourcemaps" : "^2.6.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"beepbeep": "^1.2.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "4.0.0",
"gulp-babel": "^7.0.1",
"gulp-cached": "^1.1.1",
"gulp-clean-css": "^3.9.4",
"gulp-combine-mq": "^0.4.0",
"gulp-concat": "^2.6.1",
"gulp-css-globbing": "^0.2.2",
"gulp-eslint": "^3.0.1",
"gulp-insert": "^0.5.0",
"gulp-load-plugins": "^1.5.0",
"gulp-rename": "~1.2.2",
"gulp-replace": "^0.5.4",
"gulp-sass": "^3.1.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify": "^3.0.0",
"gulp-umd" : "^0.2.1",
"gulp-util" : "^3.0.8",
"gulp-watch" : "latest",
"path" : "^0.12.7",
"run-sequence" : "^1.2.2"
"gulp-umd": "^0.2.1",
"gulp-util": "^3.0.8",
"gulp-watch": "latest",
"path": "^0.12.7",
"run-sequence": "^1.2.2"
}
}

@@ -18,25 +18,37 @@ Tagify - lightweight input "tags" script

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.
Transforms an input field or a textarea into a *Tags* component, in an easy, customizable way,
with great performance and tiny code footprint.
## [Demo page](https://yaireo.github.io/tagify/)
## [Documentation & Demos](https://yaireo.github.io/tagify)
## Selling points
* supports whitelist (with native suggestions dropdown as-you-type)
* supports blacklists
* JS file is under 150 very readiable lines of code
* JS weights less than ~5kb
* SCSS file is ~2kb of highly readable and flexible code
* JS weights less than ~12kb (less than 900 easily understandale lines of code)
* SCSS file is ~6kb of well-crafted flexible code
* Easily change direction to RTL via the SCSS file only
* No other inputs are used beside the original, and its value is kept in sync
* Easily customized
* Exposed custom events (Add, Remove, Invalid, Duplicate)
* For Internet Explorer 11 use "tagify.polyfills.js" under "/dist"
## What can Tagify do
* Can be applied on input & textarea elements
* Supports whitelist
* Supports blacklists
* Shows suggestions selectbox (flexiable settings & styling)
* Auto-complete input as-you-type (whitelist first match)
* Can paste in multiple values ("tag 1, tag 2, tag 3")
* Tags can be created by Regex delimiter or by pressing the "Enter" key / focusing of the input
* Validate tags by Regex pattern
* Supports read-only mode to the whole componenet or per-tag
* Each tag can have any properties desired (class, data-whatever, readonly...)
* Automatically disallow duplicate tags (vis "settings" object)
* Tags can be created by commas *or* by pressing the "Enter" key
* Tags can be trimmed via `hellip` by giving `max-width` to the `tag` element in your `CSS`
* Easily customized
* Exposed custom events
## building the project
Simply run `gulp` in your terminal, from the project's path (Gulp should be installed first)
## Building the project
Simply run `gulp` in your terminal, from the project's path ([Gulp](https://gulpjs.com) should be installed first).
Source files are this path `/src/`
Output files, which are automatically generated using Gulp, are in `/dist/`;
The rest of the files are most likely irrelevant.

@@ -48,6 +60,6 @@ ## Basic usage

<input name='tags' placeholder='write some tags' value='foo, bar,buzz'>
<textarea name='tags' placeholder='write some tags'>foo, bar,buzz</textarea>
<textarea name='tags' pattern=".{3,}" placeholder='write some tags'>foo, bar,buzz</textarea>
```
what you need to do to convert that nice input into "tags" is simply select your input/textarea and run `tagify()`:
What you need to do to convert that nice input into "tags" is simply select your input/textarea and run `tagify()`:

@@ -68,8 +80,133 @@ ```javascript

// listen to custom tags' events such as 'add' or 'remove'
tagify1.on('remove', ()=>{
// listen to custom tags' events such as 'add' or 'remove' (see full list below).
// listeners are chainable
tagify.on('remove', onTagRemoved)
.on('add', onTagAdded);
function onTagRemoved(e){
console.log(e, e.detail);
});
// remove listener after first tag removal
tagify.off('remove', onTagRemoved);
}
function onTagAdded(e){
// do whatever
}
```
The value of the Tagify component can be accessed like so:
```javascript
var tagify = new Tagify(...);
console.log( tagify.value )
// [{"value":"tag1"}, {"value":"tag2"}, ...]
```
If the Tags were added with custom properties, the *value* output might look something like this:
```javascript
tagify.value
// [{ "value":"tag1", "class":"red", "id":1}, ...]
```
### Tags with properties ([example](https://yaireo.github.io/tagify#section-extra-properties))
The below example will populate the Tags component with 2 tags, each with specific attributes & values.
the `addTags` method accepts an Array of Objects with **any** key/value, as long as the `value` key is defined.
```html
<input placeholder="add tags">
```javascript
var allowedTags = [
{
"value" : "apple",
"data-id" : 3,
"class" : 'color-green'
},
{
"value" : "orange",
"data-id" : 56,
"class" : 'color-orange'
},
{
"value" : "passion fruit",
"data-id" : 17,
"class" : 'color-purple'
},
{
"value" : "banana",
"data-id" : 12,
"class" : 'color-yellow'
},
{
"value" : "paprika",
"data-id" : 25,
"class" : 'color-red'
}
];
var input = document.querySelector('input'),
tagify = new Tagify(input,
whitelist : allowedTags
);
// Add the first 2 tags from the "allowedTags" Array
tagify.addTags( allowedTags.slice(0,2) )
```
The above will prepend a "tags" element before the original input element:
```html
<tags class="tagify">
<tag readonly="true" class="color-red" data-id="8" value="strawberry">
<x></x><div><span title="strawberry">strawberry</span></div>
</tag>
<tag readonly="true" class="color-darkblue" data-id="6" value="blueberry">
<x></x><div><span title="blueberry">blueberry</span></div>
</tag>
<div contenteditable data-placeholder="add tags" class="tagify--input"></div>
</tags>
<input placeholder="add tags">
```
### Suggestions selectbox
The suggestions selectbox is shown is a whitelist Array of Strings or Objects was passed in the settings when the Tagify instance was created.
Suggestions list will only be rendered if there were at least two sugegstions found.
Matching suggested values is case-insensetive.
The selectbox dropdown will be appended to the document's "body" element and will be positioned under the element.
Using the keyboard arrows up/down will highlight an option from the list, and hitting the Enter key to select.
It is possible to tweak the selectbox dropdown via 2 settings:
- enabled - this is a numeral value which tells Tagify when to show the suggestions dropdown, when a minimum of N characters were typed.
- maxItems - Limits the number of items the suggestions selectbox will render
```javascript
var input = document.querySelector('input'),
tagify = new Tagify(input,
whitelist : ['aaa', 'aaab', 'aaabb', 'aaabc', 'aaabd', 'aaabe', 'aaac', 'aaacc'],
dropdown : {
classname : "color-blue",
enabled : 3,
maxItems : 5
}
);
```
Will render:
```html
<div class="tagify__dropdown" style="left: 993.5px; top: 106.375px; width: 616px;">
<div class="tagify__dropdown__item" value="aaab">aaab</div>
<div class="tagify__dropdown__item" value="aaabb">aaabb</div>
<div class="tagify__dropdown__item" value="aaabc">aaabc</div>
<div class="tagify__dropdown__item" value="aaabd">aaabd</div>
<div class="tagify__dropdown__item" value="aaabe">aaabe</div>
</div>
```
### jQuery plugin version (jQuery.tagify.js)

@@ -85,34 +222,3 @@

Now markup be like:
```html
<tags>
<tag>
<x></x>
<div><span title="css">css</span></div>
</tag>
<tag>
<x></x>
<div><span title="html">html</span></div>
</tag>
<tag>
<x></x>
<div><span title="javascript">javascript</span></div>
</tag>
<div>
<input list="tagsSuggestions3l9nbieyr" class="input placeholder">
<datalist id="tagsSuggestions3l9nbieyr">
<label> select from the list:
<select>
<option value=""></option>
<option>foo</option>
<option>bar</option>
</select>
</label>
</datalist><span>write some tags</span>
</div>
<input name="tags" placeholder="write some tags" value="foo, bar,buzz">
</tags>
```
## Methods

@@ -122,6 +228,7 @@

--------------- | --------------------------------------------------------------------------
destroy | if called, will revert the input back as it was before Tagify was applied
removeAllTags | removes all tags and rests the original input tag's value property
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.)
## Exposed events

@@ -144,13 +251,16 @@

------------------- | ---------- | ----------- | --------------------------------------------------------------------------
delimiters | String | "," | [regex] split tags by any of these delimiters. Example: Space or Coma - ", "
pattern | String | "" | Validate the input by REGEX pattern (can also be applied on the input itself as an attribute)
delimiters | String | "," | [regex] split tags by any of these delimiters. Example: ",| |."
pattern | String | null | Validate input by REGEX pattern (can also be applied on the input itself as an attribute) Ex: /[1-9]/
duplicates | Boolean | false | (flag) should duplicate tags be allowed or not
enforeWhitelist | Boolean | false | should ONLY use tags allowed in whitelist
autocomplete | Boolean | true | show native suggeestions list, as you type
enforceWhitelist | Boolean | false | should ONLY use tags allowed in whitelist
autocomplete | Boolean | true | tries to autocomplete the input's value while typing (match from whitelist)
whitelist | Array | [] | an array of tags which only they are allowed
blacklist | Array | [] | an array of tags which aren't allowed
addTagOnBlur | Boolean | true | automatically adds the text which was inputed as a tag when blur event happens
callbacks | Object | {} | exposed callbacks object to be triggered on events: 'add' / 'remove' tags
maxTags | Number | Infinity | max number of tags
suggestionsMinChars | Number | 2 | minimum characters to input which shows the sugegstions list
dropdown.enabled | Number | 2 | minimum characters to input which shows the suggestions list dropdown
dropdown.maxItems | Number | 10 | maximum items to show in the suggestions list dropdown
dropdown.classname | String | "" | custom class name for the dropdown suggestions selectbox

@@ -8,14 +8,16 @@ function Tagify( input, settings ){

this.settings = this.extend({}, settings, this.DEFAULTS);
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( 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){}
try { this.settings.pattern = new RegExp(input.pattern) }
catch(e){}
if( settings && settings.delimiters ){
try {
this.settings.delimiters = new RegExp("[" + settings.delimiters + "]");
} 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){}
}

@@ -25,48 +27,83 @@

this.value = []; // An array holding all the (currently used) tags
// 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.extend(this, new this.EventDispatcher(this));
this.build(input);
this.events();
this.events.customBinding.call(this);
this.events.binding.call(this);
}
Tagify.prototype = {
isIE : window.document.documentMode,
DEFAULTS : {
delimiters : ",", // [regex] split tags by any of these delimiters
pattern : "", // pattern to validate input by
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
enforeWhitelist : false, // flag - should ONLY use tags allowed in whitelist
autocomplete : true, // flag - show native suggeestions list as you type
whitelist : [], // is this list has any items, then only allow tags from this list
blacklist : [], // a list of non-allowed tags
maxTags : Infinity, // maximum number of tags
suggestionsMinChars : 2 // minimum characters to input to see sugegstions list
enforceWhitelist : false, // flag - should ONLY use tags allowed in whitelist
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
}
},
customEventsList : ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'],
/**
* utility method
* https://stackoverflow.com/a/35385518/104380
* @param {String} s [HTML string]
* @return {Object} [DOM node]
*/
parseHTML(s){
var parser = new DOMParser(),
node = parser.parseFromString(s.trim(), "text/html");
return node.body.firstElementChild;
},
// https://stackoverflow.com/a/25396011/104380
escapeHtml(s){
var text = document.createTextNode(s);
var p = document.createElement('p');
p.appendChild(text);
return p.innerHTML;
},
/**
* builds the HTML of this component
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
*/
build : function( input ){
build( input ){
var that = this,
value = input.value,
inputHTML = '<div><input list="tagifySuggestions'+ this.id +'" class="placeholder"/><span>'+ input.placeholder +'</span></div>';
template = `
<tags class="tagify ${input.className} ${this.settings.readonly ? 'readonly' : ''}">
<div contenteditable data-placeholder="${input.placeholder}" class="tagify--input"></div>
</tags>`;
this.DOM.originalInput = input;
this.DOM.scope = document.createElement('tags');
this.DOM.scope.innerHTML = inputHTML;
this.DOM.input = this.DOM.scope.querySelector('input');
if( this.settings.readonly )
this.DOM.scope.classList.add('readonly')
this.DOM.scope = this.parseHTML(template);
this.DOM.input = this.DOM.scope.querySelector('[contenteditable]');
input.parentNode.insertBefore(this.DOM.scope, input);
this.DOM.scope.appendChild(input);
// if "autocomplete" flag on toggeled & "whitelist" has items, build suggestions list
if( this.settings.autocomplete && this.settings.whitelist.length )
this.DOM.datalist = this.buildDataList();
if( this.settings.dropdown.enabled && this.settings.whitelist.length ){
this.dropdown.init.call(this);
}
// if the original input already had any value (tags)
if( value )
this.addTag(value).forEach(function(tag){
this.addTags(value).forEach(tag => {
tag && tag.classList.add('tagify--noAnim');

@@ -79,4 +116,3 @@ });

*/
destroy : function(){
this.DOM.scope.parentNode.appendChild(this.DOM.originalInput);
destroy(){
this.DOM.scope.parentNode.removeChild(this.DOM.scope);

@@ -87,18 +123,29 @@ },

* Merge two objects into a new one
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})
*/
extend : function(o, o1, o2){
extend(o, o1, o2){
if( !(o instanceof Object) ) o = {};
if( o2 ){
copy(o, o1);
if( o2 )
copy(o, o2)
copy(o, o1)
}
else
copy(o, o1)
function isObject(obj) {
var type = Object.prototype.toString.call(obj);
return obj === Object(obj) && type != '[object Array]' && type != '[object Function]';
};
function copy(a,b){
// copy o2 to o
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
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];
}
}

@@ -112,3 +159,3 @@

*/
EventDispatcher : function(){
EventDispatcher( instance ){
// Create a DOM EventTarget object

@@ -118,4 +165,14 @@ var target = document.createTextNode('');

// Pass EventTarget interface calls to DOM EventTarget object
this.off = target.removeEventListener.bind(target);
this.on = target.addEventListener.bind(target);
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){

@@ -125,4 +182,5 @@ var e;

if( this.isJQueryPlugin )
$(this.DOM.originalInput).triggerHandler(eventName, [data])
if( instance.settings.isJQueryPlugin ){
$(instance.DOM.originalInput).triggerHandler(eventName, [data])
}
else{

@@ -132,6 +190,3 @@ try {

}
catch(err){
e = document.createEvent("Event");
e.initEvent("toggle", false, false);
}
catch(err){ console.warn(err) }
target.dispatchEvent(e);

@@ -145,105 +200,117 @@ }

*/
events : function(){
var that = this,
events = {
// event name / event callback / element to be listening to
paste : ['onPaste' , 'input'],
focus : ['onFocusBlur' , 'input'],
blur : ['onFocusBlur' , 'input'],
input : ['onInput' , 'input'],
keydown : ['onKeydown' , 'input'],
click : ['onClickScope' , 'scope']
},
customList = ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'];
events : {
// bind custom events which were passed in the settings
customBinding(){
this.customEventsList.forEach(name => {
this.on(name, this.settings.callbacks[name])
})
},
for( var e in events )
this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].bind(this));
binding( bindUnbind = 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';
customList.forEach(function(name){
that.on(name, that.settings.callbacks[name])
})
for( var eventName in _CBR ){
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);
}
if( this.isJQueryPlugin )
$(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(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));
/**
* DOM events callbacks
*/
callbacks : {
onFocusBlur : function(e){
var text = e.target.value.trim();
if( e.type == "focus" )
e.target.className = 'input';
else if( e.type == "blur" && text ){
if( this.addTag(text).length )
e.target.value = '';
if( this.settings.isJQueryPlugin )
$(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))
}
else{
e.target.className = 'input placeholder';
this.DOM.input.removeAttribute('style');
}
},
onKeydown : function(e){
var s = e.target.value,
that = this;
/**
* DOM events callbacks
*/
callbacks : {
onFocusBlur(e){
var s = e.target.textContent.trim();
if( e.key == "Backspace" && (s == "" || s.charCodeAt(0) == 8203) ){
this.removeTag( this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)').length - 1 );
}
if( e.key == "Escape" ){
e.target.value = '';
e.target.blur();
}
if( e.key == "Enter" ){
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
if( this.addTag(s).length )
e.target.value = '';
return false;
}
else{
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
}
},
if( e.type == "focus" ){
// e.target.classList.remove('placeholder');
}
onInput : function(e){
var value = e.target.value,
lastChar = value[value.length - 1],
isDatalistInput = !this.noneDatalistInput && value.length > 1,
showSuggestions = value.length >= this.settings.suggestionsMinChars,
datalistInDOM;
else if( e.type == "blur" && s ){
this.settings.addTagOnBlur && this.addTags(s, true).length;
}
e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';
else{
// e.target.classList.add('placeholder');
this.DOM.input.removeAttribute('style');
this.dropdown.hide.call(this);
}
},
onKeydown(e){
var s = e.target.textContent,
lastTag;
// if( value.indexOf(',') != -1 || isDatalistInput ){
if( value.slice().search(this.settings.delimiters) != -1 || isDatalistInput ){
if( this.addTag(value).length )
e.target.value = ''; // clear the input field's value
}
else if( this.settings.autocomplete && this.settings.whitelist.length ){
datalistInDOM = this.DOM.input.parentNode.contains( this.DOM.datalist );
// if sugegstions should be hidden
if( !showSuggestions && datalistInDOM )
this.DOM.input.parentNode.removeChild(this.DOM.datalist)
else if( showSuggestions && !datalistInDOM ){
this.DOM.input.parentNode.appendChild(this.DOM.datalist)
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 );
}
}
},
onPaste : function(e){
var that = this;
if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);
this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);
},
else if( e.key == 'Escape' ){
this.input.set.call(this)
e.target.blur();
}
onClickScope : function(e){
if( e.target.tagName == "TAGS" )
this.DOM.input.focus();
if( e.target.tagName == "X" ){
this.removeTag( this.getNodeIndex(e.target.parentNode) );
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(e){
var value = e.target.textContent.trim(),
showSuggestions = value.length >= this.settings.dropdown.enabled;
if( this.input.value == value ) return;
// save the value on the input state object
this.input.value = value;
this.input.normalize.call(this);
this.input.autocomplete.suggest.call(this, ''); // cleanup any possible previous suggestion
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 && this.settings.whitelist.length )
this.dropdown[showSuggestions ? "show" : "hide"].call(this, value);
},
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(e){
},
onClickScope(e){
if( e.target.tagName == "TAGS" )
this.DOM.input.focus();
else if( e.target.tagName == "X" ){
this.removeTag( e.target.parentNode );
}
}

@@ -254,30 +321,42 @@ }

/**
* Build tags suggestions using HTML datalist
* @return {[type]} [description]
* input bridge for accessing & setting
* @type {Object}
*/
buildDataList : function(){
var OPTIONS = "",
i,
datalist = document.createElement('datalist');
input : {
value : '',
set(s = ''){
this.input.value = this.DOM.input.innerHTML = s;
datalist.id = 'tagifySuggestions' + this.id;
datalist.innerHTML = "<label> \
select from the list: \
<select> \
<option value=''></option> \
[OPTIONS] \
</select> \
</label>";
if( s.length < 2 )
this.input.autocomplete.suggest.call(this, '');
},
for( i=this.settings.whitelist.length; i--; )
OPTIONS += "<option>"+ this.settings.whitelist[i] +"</option>";
// remove any child DOM elements that aren't of type TEXT (like <br>)
normalize(){
while (this.DOM.input.firstElementChild ){
this.DOM.input.removeChild(this.DOM.input.firstElementChild );
}
},
datalist.innerHTML = datalist.innerHTML.replace('[OPTIONS]', OPTIONS); // inject the options string in the right place
/**
* suggest the rest of the input's value
* @param {String} s [description]
*/
autocomplete : {
suggest(s){
if( s ) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));
else this.DOM.input.removeAttribute("data-suggest");
},
set(){
var suggestion = this.DOM.input.getAttribute('data-suggest');
// this.DOM.input.insertAdjacentHTML('afterend', datalist); // append the datalist HTML string in the Tags
return datalist;
if( suggestion && this.addTags(this.input.value + suggestion).length ){
this.input.set.call(this);
this.dropdown.hide.call(this);
}
}
}
},
getNodeIndex : function( node ){
getNodeIndex( node ){
var index = 0;

@@ -291,16 +370,38 @@ while( (node = node.previousSibling) )

/**
* Searches if any tags with a certain value exist and mark them
* @param {String / Number} value [description]
* @return {boolean} [found / not found]
* Searches if any tag with a certain value already exis
* @param {String} s [text value to search for]
* @return {boolean} [found / not found]
*/
markTagByValue : function(value){
var idx = this.value.filter(function(item){ return value.toLowerCase() === item.toLowerCase() })[0],
tag = this.DOM.scope.querySelectorAll('tag')[idx];
isTagDuplicate(s){
return this.value.some(item => s.toLowerCase() === item.value.toLowerCase());
},
if( tag ){
tag.classList.add('tagify--mark');
setTimeout(function(){ tag.classList.remove('tagify--mark') }, 2000);
/**
* 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(value, tagElm){
var tagsElms, tagsElmsLen;
if( !tagElm ){
tagsElms = this.DOM.scope.querySelectorAll('tag');
for( tagsElmsLen = tagsElms.length; tagsElmsLen--; ){
if( tagsElms[tagsElmsLen].value.toLowerCase().includes(value.toLowerCase()) )
tagElm = tagsElms[tagsElmsLen];
}
}
// check AGAIN if "tagElm" is defined
if( tagElm ){
tagElm.classList.add('tagify--mark');
setTimeout(() => { tagElm.classList.remove('tagify--mark') }, 2000);
return true;
}
else{
}
return false;

@@ -312,5 +413,5 @@ },

*/
isTagBlacklisted : function(v){
isTagBlacklisted(v){
v = v.split(' ');
return this.settings.blacklist.filter(function(x){ return v.indexOf(x) != -1 }).length;
return this.settings.blacklist.filter(x =>v.indexOf(x) != -1).length;
},

@@ -321,4 +422,8 @@

*/
isTagWhitelisted : function(v){
return this.settings.whitelist.indexOf(v) != -1;
isTagWhitelisted(v){
return this.settings.whitelist.some(item => {
var value = item.value ? item.value : item;
if( value.toLowerCase() === v.toLowerCase() )
return true;
});
},

@@ -328,73 +433,181 @@

* add a "tag" element to the "tags" component
* @param {String} value [A string of a value or multiple values]
* @return {Array} Array of DOM elements
* @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)
*/
addTag : function( value ){
addTags( tagsItems, clearInput ){
var that = this,
result;
tagElems = [];
this.DOM.input.removeAttribute('style');
value = value.trim();
/**
* 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]
*/
function 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
result = tagsItems; // the returned result
if( !value ) return [];
// no need to continue if "tagsItems" is an Array of Objects
if( isComplex )
return result;
// go over each tag and add it (if there were multiple ones)
result = value.split(this.settings.delimiters).filter(function(v){ return !!v }).map(function(v){
v = v.trim();
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties
if( !isComplex && typeof tagsItems == "string" && whitelistWithProps ){
var matchObj = this.settings.whitelist.filter( item => item.value.toLowerCase() == tagsItems.toLowerCase() )
if( that.settings.pattern && !(that.settings.pattern.test(v)) )
return false;
if( matchObj[0] ){
isComplex = true;
result = matchObj; // set the Array (with the found Object) as the new value
}
}
var tagElm = document.createElement('tag'),
isDuplicate = that.markTagByValue(v),
tagAllowed,
tagNotAllowedEventName,
maxTagsExceed = that.value.length >= that.settings.maxTags;
// if the value is a "simple" String, ex: "aaa, bbb, ccc"
if( !isComplex ){
tagsItems = tagsItems.trim();
if( !tagsItems ) return [];
if( isDuplicate ){
that.trigger('duplicate', v);
if( !that.settings.duplicates ){
return false;
// go over each tag and add it (if there were multiple ones)
result = tagsItems.split(this.settings.delimiters).map(v => ({ value:v.trim() }));
}
return result.filter(n => n); // cleanup the array from "undefined", "false" or empty items;
}
/**
* validate a tag object BEFORE the actual tag will be created & appeneded
* @param {Object} tagData [{"value":"text", "class":whatever", ...}]
* @return {Boolean/String} ["true" if validation has passed, String or "false" for any type of error]
*/
function validateTag( tagData ){
var value = tagData.value.trim(),
maxTagsExceed = this.value.length >= this.settings.maxTags,
isDuplicate,
eventName__error,
tagAllowed;
// check for empty value
if( !value )
return "empty";
// check if pattern should be used and if so, use it to test the value
if( this.settings.pattern && !(this.settings.pattern.test(value)) )
return "pattern";
// check if the tag already exists
if( this.isTagDuplicate(value) ){
this.trigger('duplicate', value);
if( !this.settings.duplicates ){
// this.markTagByValue(value, tagElm)
return "duplicate";
}
}
tagAllowed = !that.isTagBlacklisted(v) && (!that.settings.enforeWhitelist || that.isTagWhitelisted(v)) && !maxTagsExceed;
// check if the tag is allowed by the rules set
tagAllowed = !this.isTagBlacklisted(value) && (!this.settings.enforceWhitelist || this.isTagWhitelisted(value)) && !maxTagsExceed;
// check against blacklist & whitelist (if enforced)
// Check against blacklist & whitelist (if enforced)
if( !tagAllowed ){
tagElm.classList.add('tagify--notAllowed');
setTimeout(function(){ that.removeTag(that.getNodeIndex(tagElm), true) }, 1000);
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed";
// broadcast why the tag was not allowed
if( maxTagsExceed ) tagNotAllowedEventName = 'maxTagsExceed';
else if( that.isTagBlacklisted(v) ) tagNotAllowedEventName = 'blacklisted';
else if( that.settings.enforeWhitelist && !that.isTagWhitelisted(v) ) tagNotAllowedEventName = 'notWhitelisted';
if( maxTagsExceed ) eventName__error = 'maxTagsExceed';
else if( this.isTagBlacklisted(value) ) eventName__error = 'blacklisted';
else if( this.settings.enforceWhitelist && !this.isTagWhitelisted(value) ) eventName__error = 'notWhitelisted';
that.trigger(tagNotAllowedEventName, {value:v, index:that.value.length});
this.trigger(eventName__error, {value:value, index:this.value.length});
return "notAllowed";
}
// the space below is important - http://stackoverflow.com/a/19668740/104380
tagElm.innerHTML = "<x></x><div><span title='"+ v +"'>"+ v +" </span></div>";
that.DOM.scope.insertBefore(tagElm, that.DOM.input.parentNode);
return true;
}
if( tagAllowed ){
that.value.push(v);
that.update();
that.trigger('add', {value:v, index:that.value.length});
/**
* appened (validated) tag to the component's DOM scope
* @return {[type]} [description]
*/
function appendTag(tagElm){
this.DOM.scope.insertBefore(tagElm, this.DOM.input);
}
//////////////////////
tagsItems = normalizeTags.call(this, tagsItems);
tagsItems.forEach(tagData => {
var isTagValidated = validateTag.call(that, tagData);
if( isTagValidated === true || isTagValidated == "notAllowed" ){
// create the tag element
var tagElm = that.createTagElem(tagData);
// add the tag to the component's DOM
appendTag.call(that, tagElm);
// remove the tag "slowly"
if( isTagValidated == "notAllowed" ){
setTimeout(() => { that.removeTag(tagElm, true) }, 1000);
}
else{
// update state
that.value.push(tagData);
that.update();
that.trigger('add', that.extend({}, {index:that.value.length, tag:tagElm}, tagData));
tagElems.push(tagElm);
}
}
})
return tagElm;
});
if( tagsItems.length && clearInput ){
this.input.set.call(this);
}
return result.filter(function(n){ return n });
return tagElems
},
/**
* 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(tagData){
var tagElm,
escapedValue = this.escapeHtml(tagData.value),
template = `<tag>
<x></x><div><span title='${escapedValue}'>${escapedValue}</span></div>
</tag>`;
// 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);
// add any attribuets, if exists
addTagAttrs(tagElm, tagData);
return tagElm;
},
/**
* Removes a tag
* @param {Number} idx [tag index to be removed]
* @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 {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]
*/
removeTag : function( idx, silent ){
var tagElm = this.DOM.scope.children[idx];
removeTag( tagElm, silent ){
var tagData,
tagIdx = this.getNodeIndex(tagElm);
if( !tagElm) return;

@@ -407,3 +620,3 @@

// manual timeout (hack, since transitionend cannot be used because of hover)
setTimeout(function(){
setTimeout(() => {
tagElm.parentNode.removeChild(tagElm);

@@ -413,14 +626,12 @@ }, 400);

if( !silent ){
this.value.splice(idx, 1); // remove the tag from the data object
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', {value:tagElm.textContent.trim(), index:idx});
this.trigger('remove', this.extend({}, {index:tagIdx, tag:tagElm}, tagData));
}
},
removeAllTags : function(){
removeAllTags(){
this.value = [];
this.update();
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function(elm){
elm.parentNode.removeChild(elm);
});
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(elm => elm.parentNode.removeChild(elm));
},

@@ -431,5 +642,214 @@

*/
update : function(){
this.DOM.originalInput.value = this.value.join(',');
update(){
var tagsAsString = this.value.map(v => v.value).join(',');
this.DOM.originalInput.value = tagsAsString;
},
/**
* Dropdown controller
* @type {Object}
*/
dropdown : {
init(){
this.DOM.dropdown = this.dropdown.build.call(this);
},
build(){
var className = `tagify__dropdown ${this.settings.dropdown.classname}`.trim(),
template = `<div class="${className}"></div>`;
return this.parseHTML(template);
},
show( value ){
var listItems = this.dropdown.filterListItems.call(this, value),
listHTML = this.dropdown.createListHTML(listItems);
if( listItems.length && this.settings.autoComplete )
this.input.autocomplete.suggest.call(this, listItems[0].value);
if( !listHTML || listItems.length < 2 ){
this.dropdown.hide.call(this);
return;
}
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( !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(){
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
},
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( bindUnbind = true ){
// 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';
window[action]('resize', _CBR.position);
window[action]('keydown', _CBR.onKeyDown);
window[action]('click', _CBR.onClick);
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver);
// this.DOM.dropdown[action]('click', _CBR.onClick);
},
callbacks : {
onKeyDown(e){
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],
newValue = "";
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' ? "previousElementSibling" : "nextElementSibling"];
// if no element was found, loop
else
selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0];
this.dropdown.highlightOption.call(this, selectedElm);
break;
case 'Escape' :
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);
break;
}
},
onMouseOver(e){
// event delegation check
if( e.target.className.includes('__item') )
this.dropdown.highlightOption.call(this, e.target);
},
onClick(e){
if( e.target.className.includes('tagify__dropdown__item') ){
this.input.set.call(this)
this.addTags( e.target.textContent );
}
// clicked outside the dropdown, so just close it
this.dropdown.hide.call(this);
}
}
},
highlightOption( elm ){
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){
activeElm.classList.remove(className)
}
);
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className));
elm.classList.add(className);
},
/**
* returns an HTML string of the suggestions' list items
* @return {[type]} [description]
*/
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) && suggestionsCount-- )
list.push(whitelistItem);
if( suggestionsCount == 0 ) break;
}
return list;
},
/**
* Creates the dropdown items' HTML
* @param {Array} list [Array of Objects]
* @return {String}
*/
createListHTML(list){
function getItem(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("");
}
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

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