forked from filipg/gonito
239 lines
8.2 KiB
JavaScript
239 lines
8.2 KiB
JavaScript
/**
|
|
* Tagify - jQuery tags input plugin
|
|
* By Yair Even-Or (2016)
|
|
* Don't sell this code. (c)
|
|
* https://github.com/yairEO/tagify
|
|
*/
|
|
function Tagify( input, settings ){
|
|
// protection
|
|
if( !input ){
|
|
console.warn('Tagify: ', 'invalid input element ', input)
|
|
return this;
|
|
}
|
|
|
|
settings = typeof settings == 'object' ? settings : {}; // make sure settings is an 'object'
|
|
|
|
this.settings = {
|
|
duplicates : settings.duplicates || false, // flag - allow tuplicate tags
|
|
enforeWhitelist : settings.enforeWhitelist || false, // flag - should ONLY use tags allowed in whitelist
|
|
autocomplete : settings.autocomplete || true, // flag - show native suggeestions list as you type
|
|
whitelist : settings.whitelist || [], // is this list has any items, then only allow tags from this list
|
|
blacklist : settings.blacklist || [] // a list of non-allowed tags
|
|
};
|
|
|
|
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.build(input);
|
|
this.events();
|
|
}
|
|
|
|
Tagify.prototype = {
|
|
build : function( input ){
|
|
var that = this,
|
|
value = input.value;
|
|
|
|
this.DOM.originalInput = input;
|
|
this.DOM.scope = document.createElement('tags');
|
|
this.DOM.scope.innerHTML = '<div><input list="tagsSuggestions'+ this.id +'" class="placeholder"/><span>'+ input.placeholder +'</span></div>';
|
|
|
|
this.DOM.input = this.DOM.scope.querySelector('input');
|
|
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.buildDataList();
|
|
|
|
// if the original input already had any value (tags)
|
|
if( value )
|
|
this.addTag(value).forEach(function(tag){
|
|
tag && tag.classList.add('tagify--noAnim');
|
|
});
|
|
},
|
|
|
|
/**
|
|
* DOM events binding
|
|
*/
|
|
events : function(){
|
|
var events = {
|
|
// event name / event callback / element to be listening to
|
|
focus : ['onFocusBlur' , 'input'],
|
|
blur : ['onFocusBlur' , 'input'],
|
|
input : ['onInput' , 'input'],
|
|
keydown : ['onKeydown' , 'input'],
|
|
click : ['onClickScope' , 'scope']
|
|
};
|
|
|
|
for( var e in events )
|
|
this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].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 == "" ){
|
|
e.target.className = 'input placeholder';
|
|
this.DOM.input.removeAttribute('style');
|
|
}
|
|
},
|
|
|
|
onKeydown : function(e){
|
|
var s = e.target.value;
|
|
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) )
|
|
e.target.value = '';
|
|
return false;
|
|
}
|
|
},
|
|
|
|
onInput : function(e){
|
|
var value = e.target.value,
|
|
lastChar = value[value.length - 1];
|
|
|
|
e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';
|
|
|
|
if( value.indexOf(',') != -1 ){
|
|
this.addTag( value );
|
|
e.target.value = ''; // clear the input field's value
|
|
}
|
|
},
|
|
|
|
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 = "<datalist id='tagsSuggestions"+ this.id +"'> \
|
|
<label> \
|
|
select from the list: \
|
|
<select> \
|
|
<option value=''></option> \
|
|
[OPTIONS] \
|
|
</select> \
|
|
</label> \
|
|
</datalist>";
|
|
|
|
for( i=this.settings.whitelist.length; i--; )
|
|
OPTIONS += "<option>"+ this.settings.whitelist[i] +"</option>";
|
|
|
|
datalist = datalist.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
|
|
|
|
return datalist;
|
|
},
|
|
|
|
getNodeIndex : function( node ){
|
|
var index = 0;
|
|
while( (node = node.previousSibling) )
|
|
if (node.nodeType != 3 || !/^\s*$/.test(node.data))
|
|
index++;
|
|
return index;
|
|
},
|
|
|
|
markTagByValue : function(value){
|
|
var tagIdx = this.value.findIndex(function(item){ return value.toLowerCase() === item.toLowerCase() }),
|
|
tag = this.DOM.scope.querySelectorAll('tag')[tagIdx];
|
|
|
|
if( tag ){
|
|
tag.classList.add('tagify--mark');
|
|
setTimeout(function(){ tag.classList.remove('tagify--mark') }, 2000);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* make sure the tag, or words in it, is not in the blacklist
|
|
*/
|
|
isTagWhitelisted : function(v){
|
|
return this.settings.whitelist.indexOf(v) != -1;
|
|
},
|
|
|
|
addTag : function( value ){
|
|
var that = this;
|
|
|
|
this.DOM.input.removeAttribute('style');
|
|
|
|
value = value.trim();
|
|
if( !value ) return;
|
|
|
|
return value.split(',').filter(function(v){ return !!v }).map(function(v){
|
|
var tagElm = document.createElement('tag');
|
|
v = v.trim();
|
|
|
|
if( !that.settings.duplicates && that.markTagByValue(v) )
|
|
return false;
|
|
|
|
// check against blacklist & whitelist (if enforced)
|
|
if( that.isTagBlacklisted(v) || (that.settings.enforeWhitelist && !that.isTagWhitelisted(v)) ){
|
|
tagElm.classList.add('tagify--notAllowed');
|
|
setTimeout(function(){ that.removeTag(that.getNodeIndex(tagElm)) }, 1000);
|
|
}
|
|
|
|
// 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);
|
|
|
|
that.value.push(v);
|
|
that.update();
|
|
return tagElm;
|
|
});
|
|
},
|
|
|
|
removeTag : function( idx ){
|
|
var tagElm = this.DOM.scope.children[idx];
|
|
if( !tagElm) return;
|
|
|
|
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px';
|
|
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below
|
|
tagElm.classList.add('tagify--hide');
|
|
|
|
// manual timeout (hack, since transitionend cannot be used because of hover)
|
|
setTimeout(function(){
|
|
tagElm.parentNode.removeChild(tagElm);
|
|
}, 400);
|
|
|
|
this.value.splice(idx, 1);
|
|
this.update();
|
|
},
|
|
|
|
// update the origianl (hidden) input field's value
|
|
update : function(){
|
|
this.DOM.originalInput.value = this.value.join(', ');
|
|
}
|
|
}
|