@yaireo/tagify
Advanced tools
Comparing version 2.1.9 to 2.3.0
"use strict"; | ||
/** | ||
* Tagify (v 2.1.8)- tags input component | ||
* Tagify (v 2.2.10)- tags input component | ||
* By Yair Even-Or (2016) | ||
@@ -9,964 +9,931 @@ * Don't sell this code. (c) | ||
*/ | ||
;(function ($) { | ||
// just a jQuery wrapper for the vanilla version of this component | ||
$.fn.tagify = function () { | ||
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
; | ||
return this.each(function () { | ||
var $input = $(this), | ||
tagify; | ||
(function ($) { | ||
// just a jQuery wrapper for the vanilla version of this component | ||
$.fn.tagify = function () { | ||
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return this.each(function () { | ||
var $input = $(this), | ||
tagify; | ||
if ($input.data("tagify")) // don't continue if already "tagified" | ||
return this; | ||
settings.isJQueryPlugin = true; | ||
tagify = new Tagify($input[0], settings); | ||
$input.data("tagify", tagify); | ||
}); | ||
}; | ||
if ($input.data("tagify")) // don't continue if already "tagified" | ||
return this; | ||
function Tagify(input, settings) { | ||
// protection | ||
if (!input) { | ||
console.warn('Tagify: ', 'invalid input element ', input); | ||
return this; | ||
} | ||
settings.isJQueryPlugin = true; | ||
tagify = new Tagify($input[0], settings); | ||
$input.data("tagify", tagify); | ||
}); | ||
}; | ||
this.settings = this.extend({}, this.DEFAULTS, settings); | ||
this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component | ||
function Tagify(input, settings) { | ||
// protection | ||
if (!input) { | ||
console.warn('Tagify: ', 'invalid input element ', input); | ||
return this; | ||
} | ||
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false | ||
this.settings = this.extend({}, this.DEFAULTS, settings); | ||
this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component | ||
if (input.pattern) try { | ||
this.settings.pattern = new RegExp(input.pattern); | ||
} catch (e) {} // Convert the "delimiters" setting into a REGEX object | ||
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false | ||
if (input.pattern) try { | ||
this.settings.pattern = new RegExp(input.pattern); | ||
} catch (e) {} | ||
// Convert the "delimiters" setting into a REGEX object | ||
if (this.settings && this.settings.delimiters) { | ||
try { | ||
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g"); | ||
} catch (e) {} | ||
} | ||
this.value = []; // tags' data | ||
// events' callbacks references will be stores here, so events could be unbinded | ||
this.listeners = {}; | ||
this.DOM = {}; // Store all relevant DOM elements in an Object | ||
this.extend(this, new this.EventDispatcher(this)); | ||
this.build(input); | ||
this.loadOriginalValues(); | ||
this.events.customBinding.call(this); | ||
this.events.binding.call(this); | ||
if (this.settings && this.settings.delimiters) { | ||
try { | ||
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g"); | ||
} catch (e) {} | ||
} | ||
Tagify.prototype = { | ||
isIE: window.document.documentMode, | ||
this.value = []; // tags' data | ||
// events' callbacks references will be stores here, so events could be unbinded | ||
TEXTS: { | ||
empty: "empty", | ||
exceed: "number of tags exceeded", | ||
pattern: "pattern mismatch", | ||
duplicate: "already exists", | ||
notAllowed: "not allowed" | ||
}, | ||
this.listeners = {}; | ||
this.DOM = {}; // Store all relevant DOM elements in an Object | ||
DEFAULTS: { | ||
delimiters: ",", // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |." | ||
pattern: null, // RegEx pattern to validate input by. Ex: /[1-9]/ | ||
maxTags: Infinity, // Maximum number of tags | ||
callbacks: {}, // Exposed callbacks object to be triggered on certain events | ||
addTagOnBlur: true, // Flag - automatically adds the text which was inputed as a tag when blur event happens | ||
duplicates: false, // Flag - allow tuplicate tags | ||
whitelist: [], // Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting) | ||
blacklist: [], // A list of non-allowed tags | ||
enforceWhitelist: false, // Flag - Only allow tags allowed in whitelist | ||
keepInvalidTags: false, // Flag - if true, do not remove tags which did not pass validation | ||
autoComplete: true, // Flag - tries to autocomplete the input's value while typing | ||
mapValueToProp: "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value" | ||
dropdown: { | ||
classname: '', | ||
enabled: 2, // minimum input characters needs to be typed for the dropdown to show | ||
maxItems: 10, | ||
itemTemplate: '' | ||
} | ||
}, | ||
this.extend(this, new this.EventDispatcher(this)); | ||
this.build(input); | ||
this.loadOriginalValues(); | ||
this.events.customBinding.call(this); | ||
this.events.binding.call(this); | ||
} | ||
customEventsList: ['add', 'remove', 'invalid'], | ||
Tagify.prototype = { | ||
isIE: window.document.documentMode, | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility | ||
TEXTS: { | ||
empty: "empty", | ||
exceed: "number of tags exceeded", | ||
pattern: "pattern mismatch", | ||
duplicate: "already exists", | ||
notAllowed: "not allowed" | ||
}, | ||
DEFAULTS: { | ||
delimiters: ",", | ||
// [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |." | ||
pattern: null, | ||
// RegEx pattern to validate input by. Ex: /[1-9]/ | ||
maxTags: Infinity, | ||
// Maximum number of tags | ||
callbacks: {}, | ||
// Exposed callbacks object to be triggered on certain events | ||
addTagOnBlur: true, | ||
// Flag - automatically adds the text which was inputed as a tag when blur event happens | ||
duplicates: false, | ||
// Flag - allow tuplicate tags | ||
whitelist: [], | ||
// Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting) | ||
blacklist: [], | ||
// A list of non-allowed tags | ||
enforceWhitelist: false, | ||
// Flag - Only allow tags allowed in whitelist | ||
keepInvalidTags: false, | ||
// Flag - if true, do not remove tags which did not pass validation | ||
autoComplete: true, | ||
// Flag - tries to autocomplete the input's value while typing | ||
dropdown: { | ||
classname: '', | ||
enabled: 2, | ||
// minimum input characters needs to be typed for the dropdown to show | ||
maxItems: 10, | ||
itemTemplate: '' | ||
} | ||
}, | ||
customEventsList: ['add', 'remove', 'invalid'], | ||
/** | ||
* utility method | ||
* https://stackoverflow.com/a/35385518/104380 | ||
* @param {String} s [HTML string] | ||
* @return {Object} [DOM node] | ||
*/ | ||
parseHTML: function parseHTML(s) { | ||
var parser = new DOMParser(), | ||
node = parser.parseFromString(s.trim(), "text/html"); | ||
/** | ||
* utility method | ||
* https://stackoverflow.com/a/35385518/104380 | ||
* @param {String} s [HTML string] | ||
* @return {Object} [DOM node] | ||
*/ | ||
parseHTML: function parseHTML(s) { | ||
var parser = new DOMParser(), | ||
node = parser.parseFromString(s.trim(), "text/html"); | ||
return node.body.firstElementChild; | ||
}, | ||
// https://stackoverflow.com/a/25396011/104380 | ||
escapeHtml: function escapeHtml(s) { | ||
var text = document.createTextNode(s), | ||
p = document.createElement('p'); | ||
p.appendChild(text); | ||
return p.innerHTML; | ||
}, | ||
return node.body.firstElementChild; | ||
}, | ||
/** | ||
* builds the HTML of this component | ||
* @param {Object} input [DOM element which would be "transformed" into "Tags"] | ||
*/ | ||
build: function build(input) { | ||
var that = this, | ||
DOM = this.DOM, | ||
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>"; | ||
DOM.originalInput = input; | ||
DOM.scope = this.parseHTML(template); | ||
DOM.input = DOM.scope.querySelector('[contenteditable]'); | ||
input.parentNode.insertBefore(DOM.scope, input); | ||
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) { | ||
this.dropdown.init.call(this); | ||
} | ||
// https://stackoverflow.com/a/25396011/104380 | ||
escapeHtml: function escapeHtml(s) { | ||
var text = document.createTextNode(s), | ||
p = document.createElement('p'); | ||
p.appendChild(text); | ||
return p.innerHTML; | ||
}, | ||
input.autofocus && DOM.input.focus(); | ||
}, | ||
/** | ||
* Reverts back any changes made by this component | ||
*/ | ||
destroy: function destroy() { | ||
this.DOM.scope.parentNode.removeChild(this.DOM.scope); | ||
}, | ||
/** | ||
* builds the HTML of this component | ||
* @param {Object} input [DOM element which would be "transformed" into "Tags"] | ||
*/ | ||
build: function build(input) { | ||
var that = this, | ||
DOM = this.DOM, | ||
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>"; | ||
/** | ||
* Merge two objects into a new one | ||
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}}) | ||
*/ | ||
extend: function extend(o, o1, o2) { | ||
if (!(o instanceof Object)) o = {}; | ||
copy(o, o1); | ||
if (o2) copy(o, o2); | ||
DOM.originalInput = input; | ||
DOM.scope = this.parseHTML(template); | ||
DOM.input = DOM.scope.querySelector('[contenteditable]'); | ||
input.parentNode.insertBefore(DOM.scope, input); | ||
function isObject(obj) { | ||
var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1); | ||
return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement'; | ||
} | ||
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) { | ||
this.dropdown.init.call(this); | ||
} | ||
; | ||
input.autofocus && DOM.input.focus(); | ||
}, | ||
function copy(a, b) { | ||
// copy o2 to o | ||
for (var key in b) { | ||
if (b.hasOwnProperty(key)) { | ||
if (isObject(b[key])) { | ||
if (!isObject(a[key])) { | ||
a[key] = Object.assign({}, b[key]); | ||
} else copy(a[key], b[key]); | ||
} else a[key] = b[key]; | ||
} | ||
} | ||
} | ||
return o; | ||
}, | ||
/** | ||
* Reverts back any changes made by this component | ||
*/ | ||
destroy: function destroy() { | ||
this.DOM.scope.parentNode.removeChild(this.DOM.scope); | ||
}, | ||
/** | ||
* A constructor for exposing events to the outside | ||
*/ | ||
EventDispatcher: function EventDispatcher(instance) { | ||
// Create a DOM EventTarget object | ||
var target = document.createTextNode(''); // Pass EventTarget interface calls to DOM EventTarget object | ||
this.off = function (name, cb) { | ||
if (cb) target.removeEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
/** | ||
* Merge two objects into a new one | ||
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}}) | ||
*/ | ||
extend: function extend(o, o1, o2) { | ||
if (!(o instanceof Object)) o = {}; | ||
this.on = function (name, cb) { | ||
if (cb) target.addEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
copy(o, o1); | ||
if (o2) copy(o, o2); | ||
this.trigger = function (eventName, data) { | ||
var e; | ||
if (!eventName) return; | ||
function isObject(obj) { | ||
var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1); | ||
return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement'; | ||
}; | ||
if (instance.settings.isJQueryPlugin) { | ||
$(instance.DOM.originalInput).triggerHandler(eventName, [data]); | ||
} else { | ||
try { | ||
e = new CustomEvent(eventName, { | ||
"detail": data | ||
}); | ||
} catch (err) { | ||
console.warn(err); | ||
} | ||
function copy(a, b) { | ||
// copy o2 to o | ||
for (var key in b) { | ||
if (b.hasOwnProperty(key)) { | ||
if (isObject(b[key])) { | ||
if (!isObject(a[key])) { | ||
a[key] = Object.assign({}, b[key]); | ||
} else copy(a[key], b[key]); | ||
} else a[key] = b[key]; | ||
} | ||
} | ||
} | ||
target.dispatchEvent(e); | ||
} | ||
}; | ||
}, | ||
return o; | ||
}, | ||
/** | ||
* DOM events listeners binding | ||
*/ | ||
events: { | ||
// bind custom events which were passed in the settings | ||
customBinding: function customBinding() { | ||
var _this2 = this; | ||
this.customEventsList.forEach(function (name) { | ||
_this2.on(name, _this2.settings.callbacks[name]); | ||
}); | ||
}, | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
/** | ||
* A constructor for exposing events to the outside | ||
*/ | ||
EventDispatcher: function EventDispatcher(instance) { | ||
// Create a DOM EventTarget object | ||
var target = document.createTextNode(''); | ||
// Pass EventTarget interface calls to DOM EventTarget object | ||
this.off = function (name, cb) { | ||
if (cb) target.removeEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
this.on = function (name, cb) { | ||
if (cb) target.addEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
this.trigger = function (eventName, data) { | ||
var e; | ||
if (!eventName) return; | ||
if (instance.settings.isJQueryPlugin) { | ||
$(instance.DOM.originalInput).triggerHandler(eventName, [data]); | ||
} else { | ||
try { | ||
e = new CustomEvent(eventName, { "detail": data }); | ||
} catch (err) { | ||
console.warn(err); | ||
} | ||
target.dispatchEvent(e); | ||
} | ||
}; | ||
var _CB = this.events.callbacks, | ||
// setup callback references so events could be removed later | ||
_CBR = this.listeners.main = this.listeners.main || { | ||
paste: ['input', _CB.onPaste.bind(this)], | ||
focus: ['input', _CB.onFocusBlur.bind(this)], | ||
blur: ['input', _CB.onFocusBlur.bind(this)], | ||
keydown: ['input', _CB.onKeydown.bind(this)], | ||
click: ['scope', _CB.onClickScope.bind(this)] | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
for (var eventName in _CBR) { | ||
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]); | ||
} | ||
/** | ||
* DOM events listeners binding | ||
*/ | ||
events: { | ||
// bind custom events which were passed in the settings | ||
customBinding: function customBinding() { | ||
var _this2 = this; | ||
if (bindUnbind) { | ||
// this event should never be unbinded | ||
// IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead.. | ||
this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this)); | ||
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this)); | ||
} | ||
}, | ||
this.customEventsList.forEach(function (name) { | ||
_this2.on(name, _this2.settings.callbacks[name]); | ||
}); | ||
}, | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
/** | ||
* DOM events callbacks | ||
*/ | ||
callbacks: { | ||
onFocusBlur: function onFocusBlur(e) { | ||
var s = e.target.textContent.trim(); | ||
var _CB = this.events.callbacks, | ||
// setup callback references so events could be removed later | ||
_CBR = this.listeners.main = this.listeners.main || { | ||
paste: ['input', _CB.onPaste.bind(this)], | ||
focus: ['input', _CB.onFocusBlur.bind(this)], | ||
blur: ['input', _CB.onFocusBlur.bind(this)], | ||
keydown: ['input', _CB.onKeydown.bind(this)], | ||
click: ['scope', _CB.onClickScope.bind(this)] | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
for (var eventName in _CBR) { | ||
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]); | ||
} | ||
if (bindUnbind) { | ||
// this event should never be unbinded | ||
// IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead.. | ||
this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this)); | ||
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this)); | ||
} | ||
}, | ||
/** | ||
* DOM events callbacks | ||
*/ | ||
callbacks: { | ||
onFocusBlur: function onFocusBlur(e) { | ||
var s = e.target.textContent.trim(); | ||
if (e.type == "focus") { | ||
// e.target.classList.remove('placeholder'); | ||
if (this.settings.dropdown.enabled === 0) { | ||
this.dropdown.show.call(this); | ||
} | ||
} else if (e.type == "blur" && s) { | ||
this.settings.addTagOnBlur && this.addTags(s, true).length; | ||
} else { | ||
// e.target.classList.add('placeholder'); | ||
this.DOM.input.removeAttribute('style'); | ||
this.dropdown.hide.call(this); | ||
} | ||
}, | ||
onKeydown: function onKeydown(e) { | ||
var s = e.target.textContent, | ||
lastTag; | ||
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) { | ||
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)'); | ||
lastTag = lastTag[lastTag.length - 1]; | ||
this.removeTag(lastTag); | ||
} else if (e.key == 'Escape' || e.key == 'Esc') { | ||
this.input.set.call(this); | ||
e.target.blur(); | ||
} else if (e.key == 'Enter') { | ||
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 | ||
this.addTags(this.input.value, true); | ||
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this); | ||
}, | ||
onInput: function onInput(e) { | ||
var value = this.input.normalize.call(this), | ||
showSuggestions = value.length >= this.settings.dropdown.enabled; | ||
if (!value) { | ||
this.input.set.call(this, ''); | ||
return; | ||
} | ||
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead | ||
// save the value on the input's State object | ||
this.input.set.call(this, value, false); | ||
if (value.search(this.settings.delimiters) != -1) { | ||
if (this.addTags(value).length) { | ||
this.input.set.call(this); // clear the input field's value | ||
} | ||
} else if (this.settings.dropdown.enabled >= 0) { | ||
this.dropdown[showSuggestions ? "show" : "hide"].call(this, value); | ||
} | ||
}, | ||
onInputIE: function onInputIE(e) { | ||
var _this = this; | ||
// for the "e.target.textContent" to be changed, the browser requires a small delay | ||
setTimeout(function () { | ||
_this.events.callbacks.onInput.call(_this, e); | ||
}); | ||
}, | ||
onPaste: function onPaste(e) {}, | ||
onClickScope: function onClickScope(e) { | ||
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") { | ||
this.removeTag(e.target.parentNode); | ||
} | ||
} | ||
if (e.type == "focus") { | ||
// e.target.classList.remove('placeholder'); | ||
if (this.settings.dropdown.enabled === 0) { | ||
this.dropdown.show.call(this); | ||
} | ||
} else if (e.type == "blur" && s) { | ||
this.settings.addTagOnBlur && this.addTags(s, true).length; | ||
} else { | ||
// e.target.classList.add('placeholder'); | ||
this.DOM.input.removeAttribute('style'); | ||
this.dropdown.hide.call(this); | ||
} | ||
}, | ||
onKeydown: function onKeydown(e) { | ||
var s = e.target.textContent, | ||
lastTag; | ||
/** | ||
* If the original input had an values, add them as tags | ||
*/ | ||
loadOriginalValues: function loadOriginalValues() { | ||
var value = this.DOM.originalInput.value, | ||
values; | ||
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) { | ||
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)'); | ||
lastTag = lastTag[lastTag.length - 1]; | ||
this.removeTag(lastTag); | ||
} else if (e.key == 'Escape' || e.key == 'Esc') { | ||
this.input.set.call(this); | ||
e.target.blur(); | ||
} else if (e.key == 'Enter') { | ||
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 | ||
// if the original input already had any value (tags) | ||
if (!value) return; | ||
this.addTags(value).forEach(function (tag) { | ||
tag && tag.classList.add('tagify--noAnim'); | ||
}); | ||
this.addTags(this.input.value, true); | ||
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this); | ||
}, | ||
onInput: function onInput(e) { | ||
var value = this.input.normalize.call(this), | ||
showSuggestions = value.length >= this.settings.dropdown.enabled; | ||
if (!value) { | ||
this.input.set.call(this, ''); | ||
return; | ||
} | ||
/** | ||
* input bridge for accessing & setting | ||
* @type {Object} | ||
*/ | ||
input: { | ||
value: '', | ||
set: function set() { | ||
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead | ||
// save the value on the input's State object | ||
this.input.value = s; | ||
this.input.set.call(this, value, false); | ||
if (updateDOM) this.DOM.input.innerHTML = s; | ||
if (!s) this.dropdown.hide.call(this); | ||
if (s.length < 2) this.input.autocomplete.suggest.call(this, ''); | ||
this.input.validate.call(this); | ||
}, | ||
// https://stackoverflow.com/a/3866442/104380 | ||
setRangeAtEnd: function setRangeAtEnd() { | ||
var range, selection; | ||
if (!document.createRange) return; | ||
range = document.createRange(); | ||
range.selectNodeContents(this.DOM.input); | ||
range.collapse(false); | ||
selection = window.getSelection(); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
}, | ||
/** | ||
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()" | ||
*/ | ||
validate: function validate() { | ||
var isValid = !this.input.value || this.validateTag.call(this, this.input.value); | ||
this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true); | ||
}, | ||
// remove any child DOM elements that aren't of type TEXT (like <br>) | ||
normalize: function normalize() { | ||
var clone = this.DOM.input.cloneNode(true), | ||
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters | ||
while (clone.firstElementChild) { | ||
v += clone.firstElementChild.textContent; | ||
clone.removeChild(clone.firstElementChild); | ||
} | ||
return v.replace(/^\s+/, ""); // trimLeft | ||
}, | ||
/** | ||
* suggest the rest of the input's value | ||
* @param {String} s [description] | ||
*/ | ||
autocomplete: { | ||
suggest: function suggest(s) { | ||
if (!this.DOM.input.value) return; // do not suggest anything for empty input | ||
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest"); | ||
}, | ||
set: function set(s) { | ||
var dataSuggest = this.DOM.input.getAttribute('data-suggest'), | ||
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null); | ||
if (suggestion) { | ||
this.input.set.call(this, suggestion); | ||
this.input.autocomplete.suggest.call(this, ''); | ||
this.dropdown.hide.call(this); | ||
this.input.setRangeAtEnd.call(this); | ||
} | ||
// if( suggestion && this.addTags(this.input.value + suggestion).length ){ | ||
// this.input.set.call(this); | ||
// this.dropdown.hide.call(this); | ||
// } | ||
} | ||
if (value.search(this.settings.delimiters) != -1) { | ||
if (this.addTags(value).length) { | ||
this.input.set.call(this); // clear the input field's value | ||
} | ||
} else if (this.settings.dropdown.enabled >= 0) { | ||
this.dropdown[showSuggestions ? "show" : "hide"].call(this, value); | ||
} | ||
}, | ||
onInputIE: function onInputIE(e) { | ||
var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay | ||
getNodeIndex: function getNodeIndex(node) { | ||
var index = 0; | ||
while (node = node.previousSibling) { | ||
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++; | ||
}return index; | ||
}, | ||
/** | ||
* Searches if any tag with a certain value already exis | ||
* @param {String} s [text value to search for] | ||
* @return {int} [Position index of the tag. -1 is returned if tag is not found.] | ||
*/ | ||
isTagDuplicate: function isTagDuplicate(s) { | ||
return this.value.findIndex(function (item) { | ||
return s.toLowerCase() === item.value.toLowerCase(); | ||
}); | ||
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase()); | ||
setTimeout(function () { | ||
_this.events.callbacks.onInput.call(_this, e); | ||
}); | ||
}, | ||
onPaste: function onPaste(e) {}, | ||
onClickScope: function onClickScope(e) { | ||
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") { | ||
this.removeTag(e.target.parentNode); | ||
} | ||
} | ||
} | ||
}, | ||
/** | ||
* If the original input had an values, add them as tags | ||
*/ | ||
loadOriginalValues: function loadOriginalValues() { | ||
var value = this.DOM.originalInput.value; // if the original input already had any value (tags) | ||
/** | ||
* Mark a tag element by its value | ||
* @param {String / Number} value [text value to search for] | ||
* @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings] | ||
* @return {boolean} [found / not found] | ||
*/ | ||
markTagByValue: function markTagByValue(value, tagElm) { | ||
var tagsElms, tagsElmsLen, tagIdx; | ||
if (!value) return; | ||
if (!tagElm) { | ||
tagIdx = this.isTagDuplicate.call(this, value); | ||
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
} | ||
try { | ||
value = JSON.parse(value); | ||
} catch (err) {} | ||
// check AGAIN if "tagElm" is defined | ||
if (tagElm) { | ||
tagElm.classList.add('tagify--mark'); | ||
setTimeout(function () { | ||
tagElm.classList.remove('tagify--mark'); | ||
}, 100); | ||
return tagElm; | ||
} | ||
this.addTags(value).forEach(function (tag) { | ||
tag && tag.classList.add('tagify--noAnim'); | ||
}); | ||
}, | ||
return false; | ||
}, | ||
/** | ||
* input bridge for accessing & setting | ||
* @type {Object} | ||
*/ | ||
input: { | ||
value: '', | ||
set: function set() { | ||
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
this.input.value = s; | ||
if (updateDOM) this.DOM.input.innerHTML = s; | ||
if (!s) this.dropdown.hide.call(this); | ||
if (s.length < 2) this.input.autocomplete.suggest.call(this, ''); | ||
this.input.validate.call(this); | ||
}, | ||
// https://stackoverflow.com/a/3866442/104380 | ||
setRangeAtEnd: function setRangeAtEnd() { | ||
var range, selection; | ||
if (!document.createRange) return; | ||
range = document.createRange(); | ||
range.selectNodeContents(this.DOM.input); | ||
range.collapse(false); | ||
selection = window.getSelection(); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
}, | ||
/** | ||
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()" | ||
*/ | ||
validate: function validate() { | ||
var isValid = !this.input.value || this.validateTag.call(this, this.input.value); | ||
this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true); | ||
}, | ||
// remove any child DOM elements that aren't of type TEXT (like <br>) | ||
normalize: function normalize() { | ||
var clone = this.DOM.input.cloneNode(true), | ||
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagBlacklisted: function isTagBlacklisted(v) { | ||
v = v.split(' '); | ||
return this.settings.blacklist.filter(function (x) { | ||
return v.indexOf(x) != -1; | ||
}).length; | ||
}, | ||
while (clone.firstElementChild) { | ||
v += clone.firstElementChild.textContent; | ||
clone.removeChild(clone.firstElementChild); | ||
} | ||
return v.replace(/^\s+/, ""); // trimLeft | ||
}, | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagWhitelisted: function isTagWhitelisted(v) { | ||
return this.settings.whitelist.some(function (item) { | ||
var value = item.value ? item.value : item; | ||
if (value.toLowerCase() === v.toLowerCase()) return true; | ||
}); | ||
/** | ||
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)") | ||
* @param {String} s [description] | ||
*/ | ||
autocomplete: { | ||
suggest: function suggest(s) { | ||
if (!s || !this.input.value) this.DOM.input.removeAttribute("data-suggest");else this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length)); | ||
}, | ||
set: function set(s) { | ||
var dataSuggest = this.DOM.input.getAttribute('data-suggest'), | ||
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null); | ||
if (suggestion) { | ||
this.input.set.call(this, suggestion); | ||
this.input.autocomplete.suggest.call(this, ''); | ||
this.dropdown.hide.call(this); | ||
this.input.setRangeAtEnd.call(this); | ||
} // if( suggestion && this.addTags(this.input.value + suggestion).length ){ | ||
// this.input.set.call(this); | ||
// this.dropdown.hide.call(this); | ||
// } | ||
/** | ||
* validate a tag object BEFORE the actual tag will be created & appeneded | ||
* @param {String} s | ||
* @return {Boolean/String} ["true" if validation has passed, String for a fail] | ||
*/ | ||
validateTag: function validateTag(s) { | ||
var value = s.trim(), | ||
maxTagsExceed = this.value.length >= this.settings.maxTags, | ||
isDuplicate, | ||
eventName__error, | ||
result = true; | ||
} | ||
} | ||
}, | ||
getNodeIndex: function getNodeIndex(node) { | ||
var index = 0; | ||
// check for empty value | ||
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; | ||
while (node = node.previousSibling) { | ||
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++; | ||
} | ||
// check if pattern should be used and if so, use it to test the value | ||
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; | ||
return index; | ||
}, | ||
// if duplicates are not allowed and there is a duplicate | ||
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed; | ||
/** | ||
* Searches if any tag with a certain value already exis | ||
* @param {String} s [text value to search for] | ||
* @return {int} [Position index of the tag. -1 is returned if tag is not found.] | ||
*/ | ||
isTagDuplicate: function isTagDuplicate(s) { | ||
return this.value.findIndex(function (item) { | ||
return s.trim().toLowerCase() === item.value.toLowerCase(); | ||
}); // return this.value.some(item => s.toLowerCase() === item.value.toLowerCase()); | ||
}, | ||
getTagIndexByValue: function getTagIndexByValue(value) { | ||
var result = []; | ||
this.DOM.scope.querySelectorAll('tag').forEach(function (tagElm, i) { | ||
if (tagElm.textContent.trim().toLowerCase() == value.toLowerCase()) result.push(i); | ||
}); | ||
return result; | ||
}, | ||
getTagElmByValue: function getTagElmByValue(value) { | ||
var tagIdx = this.getTagIndexByValue(value)[0]; | ||
return this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
}, | ||
return result; | ||
}, | ||
/** | ||
* Mark a tag element by its value | ||
* @param {String / Number} value [text value to search for] | ||
* @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings] | ||
* @return {boolean} [found / not found] | ||
*/ | ||
markTagByValue: function markTagByValue(value, tagElm) { | ||
var tagsElms, tagsElmsLen; | ||
tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined | ||
if (tagElm) { | ||
tagElm.classList.add('tagify--mark'); | ||
setTimeout(function () { | ||
tagElm.classList.remove('tagify--mark'); | ||
}, 100); | ||
return tagElm; | ||
} | ||
/** | ||
* pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words | ||
* so each item should be iterated on and a tag created for. | ||
* @return {Array} [Array of Objects] | ||
*/ | ||
normalizeTags: function normalizeTags(tagsItems) { | ||
var _this3 = this; | ||
return false; | ||
}, | ||
var whitelistWithProps = this.settings.whitelist[0] instanceof Object, | ||
isComplex = tagsItems instanceof Array && "value" in tagsItems[0], | ||
// checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
temp = []; | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagBlacklisted: function isTagBlacklisted(v) { | ||
v = v.split(' '); | ||
return this.settings.blacklist.filter(function (x) { | ||
return v.indexOf(x) != -1; | ||
}).length; | ||
}, | ||
// no need to continue if "tagsItems" is an Array of Objects | ||
if (isComplex) return tagsItems; | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagWhitelisted: function isTagWhitelisted(v) { | ||
return this.settings.whitelist.some(function (item) { | ||
var value = item.value ? item.value : item; | ||
if (value.toLowerCase() === v.toLowerCase()) return true; | ||
}); | ||
}, | ||
// if the value is a "simple" String, ex: "aaa, bbb, ccc" | ||
if (!isComplex) { | ||
if (!tagsItems.trim()) return []; | ||
/** | ||
* validate a tag object BEFORE the actual tag will be created & appeneded | ||
* @param {String} s | ||
* @return {Boolean/String} ["true" if validation has passed, String for a fail] | ||
*/ | ||
validateTag: function validateTag(s) { | ||
var value = s.trim(), | ||
maxTagsExceed = this.value.length >= this.settings.maxTags, | ||
isDuplicate, | ||
eventName__error, | ||
result = true; // check for empty value | ||
// go over each tag and add it (if there were multiple ones) | ||
tagsItems = tagsItems.split(this.settings.delimiters).filter(function (n) { | ||
return n; | ||
}).map(function (v) { | ||
return { value: v.trim() }; | ||
}); | ||
} | ||
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; // check if pattern should be used and if so, use it to test the value | ||
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate | ||
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed; | ||
return result; | ||
}, | ||
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties | ||
if (!isComplex && whitelistWithProps) { | ||
tagsItems.forEach(function (tag) { | ||
var matchObj = _this3.settings.whitelist.filter(function (WL_item) { | ||
return WL_item.value.toLowerCase() == tag.value.toLowerCase(); | ||
}); | ||
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value | ||
else temp.push(tag); | ||
}); | ||
/** | ||
* pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words | ||
* so each item should be iterated on and a tag created for. | ||
* @return {Array} [Array of Objects] | ||
*/ | ||
normalizeTags: function normalizeTags(tagsItems) { | ||
var _this3 = this; | ||
tagsItems = temp; | ||
} | ||
var whitelistWithProps = this.settings.whitelist[0] instanceof Object, | ||
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0], | ||
// checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
temp = []; // no need to continue if "tagsItems" is an Array of Objects | ||
return tagsItems; | ||
}, | ||
if (isComplex) return tagsItems; // if the value is a "simple" String, ex: "aaa, bbb, ccc" | ||
if (typeof tagsItems == 'string') { | ||
if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones) | ||
/** | ||
* add a "tag" element to the "tags" component | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects] | ||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] | ||
* @return {Array} Array of DOM elements (tags) | ||
*/ | ||
addTags: function addTags(tagsItems, clearInput) { | ||
var _this4 = this; | ||
return tagsItems.split(this.settings.delimiters).filter(function (n) { | ||
return n; | ||
}).map(function (v) { | ||
return { | ||
value: v.trim() | ||
}; | ||
}); | ||
} | ||
var tagElems = []; | ||
if (tagsItems instanceof Array) return tagsItems.map(function (v) { | ||
return { | ||
value: v.trim() | ||
}; | ||
}); // search if the tag exists in the whitelist as an Object (has props), to be able to use its properties | ||
this.DOM.input.removeAttribute('style'); | ||
if (whitelistWithProps) { | ||
tagsItems.forEach(function (tag) { | ||
var matchObj = _this3.settings.whitelist.filter(function (WL_item) { | ||
return WL_item.value.toLowerCase() == tag.value.toLowerCase(); | ||
}); | ||
tagsItems = this.normalizeTags.call(this, tagsItems); | ||
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value | ||
else temp.push(tag); | ||
}); | ||
return temp; | ||
} | ||
}, | ||
tagsItems.forEach(function (tagData) { | ||
var tagValidation, tagElm; | ||
/** | ||
* add a "tag" element to the "tags" component | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] | ||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] | ||
* @return {Array} Array of DOM elements (tags) | ||
*/ | ||
addTags: function addTags(tagsItems, clearInput) { | ||
var _this4 = this; | ||
if (typeof _this4.settings.transformTag === 'function') { | ||
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value; | ||
} | ||
var tagElems = []; | ||
this.DOM.input.removeAttribute('style'); | ||
tagsItems = this.normalizeTags.call(this, tagsItems); | ||
tagsItems.forEach(function (tagData) { | ||
var tagValidation, tagElm; | ||
tagValidation = _this4.validateTag.call(_this4, tagData.value); | ||
if (typeof _this4.settings.transformTag === 'function') { | ||
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value; | ||
} | ||
if (tagValidation !== true) { | ||
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed"; | ||
tagData.title = tagValidation; | ||
_this4.markTagByValue.call(_this4, tagData.value); | ||
_this4.trigger("invalid", { value: tagData.value, index: _this4.value.length, message: tagValidation }); | ||
} | ||
tagValidation = _this4.validateTag.call(_this4, tagData.value); | ||
// Create tag HTML element | ||
tagElm = _this4.createTagElem(tagData); | ||
tagElems.push(tagElm); | ||
if (tagValidation !== true) { | ||
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed"; | ||
tagData.title = tagValidation; | ||
// add the tag to the component's DOM | ||
appendTag.call(_this4, tagElm); | ||
_this4.markTagByValue(tagData.value); | ||
if (tagValidation === true) { | ||
// update state | ||
_this4.value.push(tagData); | ||
_this4.update(); | ||
_this4.trigger('add', _this4.extend({}, { index: _this4.value.length, tag: tagElm }, tagData)); | ||
} else if (!_this4.settings.keepInvalidTags) { | ||
// remove invalid tags (if "keepInvalidTags" is set to "false") | ||
setTimeout(function () { | ||
_this4.removeTag(tagElm, true); | ||
}, 1000); | ||
} | ||
}); | ||
_this4.trigger("invalid", { | ||
value: tagData.value, | ||
index: _this4.value.length, | ||
message: tagValidation | ||
}); | ||
} // Create tag HTML element | ||
if (tagsItems.length && clearInput) { | ||
this.input.set.call(this); | ||
} | ||
/** | ||
* appened (validated) tag to the component's DOM scope | ||
* @return {[type]} [description] | ||
*/ | ||
function appendTag(tagElm) { | ||
var insertBeforeNode = this.DOM.scope.lastElementChild; | ||
tagElm = _this4.createTagElem(tagData); | ||
tagElems.push(tagElm); // add the tag to the component's DOM | ||
if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm); | ||
} | ||
appendTag.call(_this4, tagElm); | ||
return tagElems; | ||
}, | ||
if (tagValidation === true) { | ||
// update state | ||
_this4.value.push(tagData); | ||
_this4.update(); | ||
/** | ||
* creates a DOM tag element and injects it into the component (this.DOM.scope) | ||
* @param Object} tagData [text value & properties for the created tag] | ||
* @return {Object} [DOM element] | ||
*/ | ||
createTagElem: function createTagElem(tagData) { | ||
var tagElm, | ||
v = this.escapeHtml(tagData.value), | ||
template = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>"; | ||
_this4.trigger('add', _this4.extend({}, { | ||
index: _this4.value.length, | ||
tag: tagElm | ||
}, tagData)); | ||
} else if (!_this4.settings.keepInvalidTags) { | ||
// remove invalid tags (if "keepInvalidTags" is set to "false") | ||
setTimeout(function () { | ||
_this4.removeTag(tagElm, true); | ||
}, 1000); | ||
} | ||
}); | ||
if (typeof this.settings.tagTemplate === "function") { | ||
try { | ||
template = this.settings.tagTemplate(v, tagData); | ||
} catch (err) {} | ||
} | ||
if (tagsItems.length && clearInput) { | ||
this.input.set.call(this); | ||
} | ||
/** | ||
* appened (validated) tag to the component's DOM scope | ||
* @return {[type]} [description] | ||
*/ | ||
// for a certain Tag element, add attributes. | ||
function addTagAttrs(tagElm, tagData) { | ||
var i, | ||
keys = Object.keys(tagData); | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (!tagData.hasOwnProperty(propName)) return; | ||
tagElm.setAttribute(propName, tagData[propName]); | ||
} | ||
} | ||
tagElm = this.parseHTML(template); | ||
function appendTag(tagElm) { | ||
var insertBeforeNode = this.DOM.scope.lastElementChild; | ||
if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm); | ||
} | ||
// add any attribuets, if exists | ||
addTagAttrs(tagElm, tagData); | ||
return tagElems; | ||
}, | ||
return tagElm; | ||
}, | ||
/** | ||
* creates a DOM tag element and injects it into the component (this.DOM.scope) | ||
* @param Object} tagData [text value & properties for the created tag] | ||
* @return {Object} [DOM element] | ||
*/ | ||
createTagElem: function createTagElem(tagData) { | ||
var tagElm, | ||
v = this.escapeHtml(tagData.value), | ||
template = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>"; | ||
if (typeof this.settings.tagTemplate === "function") { | ||
try { | ||
template = this.settings.tagTemplate(v, tagData); | ||
} catch (err) {} | ||
} // for a certain Tag element, add attributes. | ||
/** | ||
* Removes a tag | ||
* @param {Object} tagElm [DOM element] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
*/ | ||
removeTag: function removeTag(tagElm, silent) { | ||
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250; | ||
if (!tagElm) return; | ||
function addTagAttrs(tagElm, tagData) { | ||
var i, | ||
keys = Object.keys(tagData); | ||
var tagData, | ||
tagIdx = this.getNodeIndex(tagElm); | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (!tagData.hasOwnProperty(propName)) return; | ||
tagElm.setAttribute(propName, tagData[propName]); | ||
} | ||
} | ||
if (!tagElm) return; | ||
tagElm = this.parseHTML(template); // add any attribuets, if exists | ||
if (tranDuration && tranDuration > 10) animation();else removeNode(); | ||
addTagAttrs(tagElm, tagData); | ||
return tagElm; | ||
}, | ||
if (!silent) { | ||
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object | ||
this.update(); // update the original input with the current value | ||
this.trigger('remove', this.extend({}, { index: tagIdx, tag: tagElm }, tagData)); | ||
} | ||
/** | ||
* Removes a tag | ||
* @param {Object|String} tagElm [DOM element or a String value] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
*/ | ||
removeTag: function removeTag(tagElm, silent) { | ||
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250; | ||
if (!tagElm) return; | ||
if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm); | ||
var tagData, | ||
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable) | ||
function animation() { | ||
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px'; | ||
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below | ||
tagElm.classList.add('tagify--hide'); | ||
if (tranDuration && tranDuration > 10) animation();else removeNode(); | ||
// manual timeout (hack, since transitionend cannot be used because of hover) | ||
setTimeout(removeNode, 400); | ||
} | ||
if (!silent) { | ||
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object | ||
function removeNode() { | ||
tagElm.parentNode.removeChild(tagElm); | ||
} | ||
}, | ||
removeAllTags: function removeAllTags() { | ||
this.value = []; | ||
this.update(); | ||
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) { | ||
return elm.parentNode.removeChild(elm); | ||
}); | ||
}, | ||
this.update(); // update the original input with the current value | ||
this.trigger('remove', this.extend({}, { | ||
index: tagIdx, | ||
tag: tagElm | ||
}, tagData)); | ||
} | ||
/** | ||
* update the origianl (hidden) input field's value | ||
* see - https://stackoverflow.com/q/50957841/104380 | ||
*/ | ||
update: function update() { | ||
var _this5 = this; | ||
function animation() { | ||
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px'; | ||
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below | ||
var tagsAsString = this.value.map(function (v) { | ||
return v[_this5.settings.mapValueToProp || "value"] || v.value; | ||
}); | ||
this.DOM.originalInput.value = JSON.stringify(tagsAsString); | ||
}, | ||
tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover) | ||
setTimeout(removeNode, 400); | ||
} | ||
/** | ||
* Dropdown controller | ||
* @type {Object} | ||
*/ | ||
dropdown: { | ||
init: function init() { | ||
this.DOM.dropdown = this.dropdown.build.call(this); | ||
}, | ||
build: function build() { | ||
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(), | ||
template = "<div class=\"" + className + "\"></div>"; | ||
return this.parseHTML(template); | ||
}, | ||
show: function show(value) { | ||
var listItems, listHTML; | ||
function removeNode() { | ||
tagElm.parentNode.removeChild(tagElm); | ||
} | ||
}, | ||
removeAllTags: function removeAllTags() { | ||
this.value = []; | ||
this.update(); | ||
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) { | ||
return elm.parentNode.removeChild(elm); | ||
}); | ||
}, | ||
if (!this.settings.whitelist.length) return; | ||
/** | ||
* update the origianl (hidden) input field's value | ||
* see - https://stackoverflow.com/q/50957841/104380 | ||
*/ | ||
update: function update() { | ||
this.DOM.originalInput.value = JSON.stringify(this.value); | ||
}, | ||
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0); | ||
/** | ||
* Dropdown controller | ||
* @type {Object} | ||
*/ | ||
dropdown: { | ||
init: function init() { | ||
this.DOM.dropdown = this.dropdown.build.call(this); | ||
}, | ||
build: function build() { | ||
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(), | ||
template = "<div class=\"" + className + "\"></div>"; | ||
return this.parseHTML(template); | ||
}, | ||
show: function show(value) { | ||
var listItems, listHTML; | ||
if (!this.settings.whitelist.length) return; // if no value was supplied, show all the "whitelist" items in the dropdown | ||
// @type [Array] listItems | ||
listHTML = this.dropdown.createListHTML.call(this, listItems); | ||
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0); | ||
listHTML = this.dropdown.createListHTML.call(this, listItems); // set the first item from the suggestions list as the autocomplete value | ||
// set the first item from the suggestions list as the autocomplete value | ||
if (this.settings.autoComplete) { | ||
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : ''); | ||
} | ||
if (this.settings.autoComplete) { | ||
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : ''); | ||
} // if( !listHTML || listItems.length < 2 ){ | ||
// this.dropdown.hide.call(this); | ||
// return; | ||
// } | ||
if (!listHTML || listItems.length < 2) { | ||
this.dropdown.hide.call(this); | ||
return; | ||
} | ||
this.DOM.dropdown.innerHTML = listHTML; | ||
this.dropdown.position.call(this); | ||
this.DOM.dropdown.innerHTML = listHTML; | ||
this.dropdown.position.call(this); // if the dropdown has yet to be appended to the document, | ||
// append the dropdown to the body element & handle events | ||
// if the dropdown has yet to be appended to the document, | ||
// append the dropdown to the body element & handle events | ||
if (!this.DOM.dropdown.parentNode != document.body) { | ||
document.body.appendChild(this.DOM.dropdown); | ||
this.events.binding.call(this, false); // unbind the main events | ||
this.dropdown.events.binding.call(this); | ||
} | ||
}, | ||
hide: function hide() { | ||
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return; | ||
if (!this.DOM.dropdown.parentNode != document.body) { | ||
document.body.appendChild(this.DOM.dropdown); | ||
this.events.binding.call(this, false); // unbind the main events | ||
document.body.removeChild(this.DOM.dropdown); | ||
window.removeEventListener('resize', this.dropdown.position); | ||
this.dropdown.events.binding.call(this); | ||
} | ||
}, | ||
hide: function hide() { | ||
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return; | ||
document.body.removeChild(this.DOM.dropdown); | ||
window.removeEventListener('resize', this.dropdown.position); | ||
this.dropdown.events.binding.call(this, false); // unbind all events | ||
this.dropdown.events.binding.call(this, false); // unbind all events | ||
this.events.binding.call(this); // re-bind main events | ||
}, | ||
position: function position() { | ||
var rect = this.DOM.scope.getBoundingClientRect(); | ||
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \ | ||
this.events.binding.call(this); // re-bind main events | ||
}, | ||
position: function position() { | ||
var rect = this.DOM.scope.getBoundingClientRect(); | ||
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \ | ||
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \ | ||
width: " + rect.width + "px"; | ||
}, | ||
}, | ||
/** | ||
* @type {Object} | ||
*/ | ||
events: { | ||
/** | ||
* Events should only be binded when the dropdown is rendered and removed when isn't | ||
* @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] | ||
* @return {[type]} [description] | ||
*/ | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
/** | ||
* @type {Object} | ||
*/ | ||
events: { | ||
// references to the ".bind()" methods must be saved so they could be unbinded later | ||
var _CBR = this.listeners.dropdown = this.listeners.dropdown || { | ||
position: this.dropdown.position.bind(this), | ||
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this), | ||
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this), | ||
onClick: this.dropdown.events.callbacks.onClick.bind(this) | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
/** | ||
* Events should only be binded when the dropdown is rendered and removed when isn't | ||
* @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] | ||
* @return {[type]} [description] | ||
*/ | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
window[action]('resize', _CBR.position); | ||
window[action]('keydown', _CBR.onKeyDown); | ||
window[action]('mousedown', _CBR.onClick); | ||
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); // this.DOM.dropdown[action]('click', _CBR.onClick); | ||
}, | ||
callbacks: { | ||
onKeyDown: function onKeyDown(e) { | ||
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0], | ||
newValue = ""; | ||
// references to the ".bind()" methods must be saved so they could be unbinded later | ||
var _CBR = this.listeners.dropdown = this.listeners.dropdown || { | ||
position: this.dropdown.position.bind(this), | ||
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this), | ||
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this), | ||
onClick: this.dropdown.events.callbacks.onClick.bind(this) | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
switch (e.key) { | ||
case 'ArrowDown': | ||
case 'ArrowUp': | ||
case 'Down': // >IE11 | ||
window[action]('resize', _CBR.position); | ||
window[action]('keydown', _CBR.onKeyDown); | ||
window[action]('mousedown', _CBR.onClick); | ||
case 'Up': | ||
// >IE11 | ||
e.preventDefault(); | ||
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop | ||
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); | ||
// this.DOM.dropdown[action]('click', _CBR.onClick); | ||
}, | ||
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0]; | ||
this.dropdown.highlightOption.call(this, selectedElm, true); | ||
break; | ||
case 'Escape': | ||
case 'Esc': | ||
// IE11 | ||
this.dropdown.hide.call(this); | ||
break; | ||
callbacks: { | ||
onKeyDown: function onKeyDown(e) { | ||
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0], | ||
newValue = ""; | ||
case 'Enter': | ||
e.preventDefault(); | ||
newValue = selectedElm ? selectedElm.textContent : this.input.value; | ||
this.addTags(newValue, true); | ||
this.dropdown.hide.call(this); | ||
break; | ||
switch (e.key) { | ||
case 'ArrowDown': | ||
case 'ArrowUp': | ||
case 'Down': // >IE11 | ||
case 'Up': | ||
// >IE11 | ||
e.preventDefault(); | ||
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; | ||
case 'ArrowRight': | ||
case 'Tab': | ||
e.preventDefault(); | ||
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null); | ||
return false; | ||
} | ||
}, | ||
onMouseOver: function onMouseOver(e) { | ||
// event delegation check | ||
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target); | ||
}, | ||
onClick: function onClick(e) { | ||
var _this5 = this; | ||
// if no element was found, loop | ||
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0]; | ||
this.dropdown.highlightOption.call(this, selectedElm, true); | ||
break; | ||
case 'Escape': | ||
case 'Esc': | ||
// IE11 | ||
this.dropdown.hide.call(this); | ||
break; | ||
case 'Enter': | ||
e.preventDefault(); | ||
newValue = selectedElm ? selectedElm.textContent : this.input.value; | ||
this.addTags(newValue, true); | ||
this.dropdown.hide.call(this); | ||
break; | ||
case 'ArrowRight': | ||
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null); | ||
break; | ||
} | ||
}, | ||
onMouseOver: function onMouseOver(e) { | ||
// event delegation check | ||
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target); | ||
}, | ||
onClick: function onClick(e) { | ||
var _this6 = this; | ||
var onClickOutside = function onClickOutside() { | ||
return _this6.dropdown.hide.call(_this6); | ||
}, | ||
listItemElm; | ||
if (e.button != 0) return; // allow only mouse left-clicks | ||
if (e.target == document.documentElement) return onClickOutside(); | ||
listItemElm = [e.target, e.target.parentNode].filter(function (a) { | ||
return a.className.includes("tagify__dropdown__item"); | ||
})[0]; | ||
if (listItemElm) { | ||
this.input.set.call(this); | ||
this.addTags(listItemElm.textContent); | ||
} | ||
// clicked outside the dropdown, so just close it | ||
else onClickOutside(); | ||
} | ||
} | ||
var onClickOutside = function onClickOutside() { | ||
return _this5.dropdown.hide.call(_this5); | ||
}, | ||
listItemElm; | ||
highlightOption: function highlightOption(elm, adjustScroll) { | ||
if (!elm) return; | ||
var className = "tagify__dropdown__item--active"; | ||
if (e.button != 0) return; // allow only mouse left-clicks | ||
// for IE support, which doesn't allow "forEach" on "NodeList" Objects | ||
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) { | ||
return activeElm.classList.remove(className); | ||
}); | ||
if (e.target == document.documentElement) return onClickOutside(); | ||
listItemElm = [e.target, e.target.parentNode].filter(function (a) { | ||
return a.className.includes("tagify__dropdown__item"); | ||
})[0]; | ||
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className)); | ||
elm.classList.add(className); | ||
if (listItemElm) { | ||
this.input.set.call(this); | ||
this.addTags(listItemElm.textContent); | ||
} // clicked outside the dropdown, so just close it | ||
else onClickOutside(); | ||
} | ||
} | ||
}, | ||
highlightOption: function highlightOption(elm, adjustScroll) { | ||
if (!elm) return; | ||
var className = "tagify__dropdown__item--active"; // for IE support, which doesn't allow "forEach" on "NodeList" Objects | ||
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; | ||
}, | ||
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) { | ||
return activeElm.classList.remove(className); | ||
}); // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className)); | ||
elm.classList.add(className); | ||
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; | ||
}, | ||
/** | ||
* returns an HTML string of the suggestions' list items | ||
* @return {[type]} [description] | ||
*/ | ||
filterListItems: function filterListItems(value) { | ||
if (!value) return ""; | ||
/** | ||
* returns an HTML string of the suggestions' list items | ||
* @return {[type]} [description] | ||
*/ | ||
filterListItems: function filterListItems(value) { | ||
if (!value) return ""; | ||
var list = [], | ||
whitelist = this.settings.whitelist, | ||
suggestionsCount = this.settings.dropdown.maxItems || Infinity, | ||
whitelistItem, | ||
valueIsInWhitelist, | ||
i = 0; | ||
var list = [], | ||
whitelist = this.settings.whitelist, | ||
suggestionsCount = this.settings.dropdown.maxItems || Infinity, | ||
whitelistItem, | ||
valueIsInWhitelist, | ||
i = 0; | ||
for (; i < whitelist.length; i++) { | ||
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { | ||
value: whitelist[i] | ||
}, //normalize value as an Object | ||
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">=" | ||
// match for the value within each "whitelist" item | ||
for (; i < whitelist.length; i++) { | ||
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value: whitelist[i] }, //normalize value as an Object | ||
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">=" | ||
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem); | ||
if (suggestionsCount == 0) break; | ||
} | ||
// match for the value within each "whitelist" item | ||
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem); | ||
if (suggestionsCount == 0) break; | ||
} | ||
return list; | ||
}, | ||
return list; | ||
}, | ||
/** | ||
* Creates the dropdown items' HTML | ||
* @param {Array} list [Array of Objects] | ||
* @return {String} | ||
*/ | ||
createListHTML: function createListHTML(list) { | ||
var getItem = this.settings.dropdown.itemTemplate || function (item) { | ||
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>"; | ||
}; // for a certain Tag element, add attributes. | ||
/** | ||
* Creates the dropdown items' HTML | ||
* @param {Array} list [Array of Objects] | ||
* @return {String} | ||
*/ | ||
createListHTML: function createListHTML(list) { | ||
var getItem = this.settings.dropdown.itemTemplate || function (item) { | ||
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>"; | ||
}; | ||
function getAttributesString(item) { | ||
var i, | ||
keys = Object.keys(item), | ||
s = ""; | ||
// for a certain Tag element, add attributes. | ||
function getAttributesString(item) { | ||
var i, | ||
keys = Object.keys(item), | ||
s = ""; | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (propName != 'class' && !item.hasOwnProperty(propName)) return; | ||
s += " " + propName + (item[propName] ? "=" + item[propName] : ""); | ||
} | ||
return s; | ||
} | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (propName != 'class' && !item.hasOwnProperty(propName)) return; | ||
s += " " + propName + (item[propName] ? "=" + item[propName] : ""); | ||
} | ||
return list.map(getItem).join(""); | ||
} | ||
return s; | ||
} | ||
}; | ||
return list.map(getItem).join(""); | ||
} | ||
} | ||
}; | ||
})(jQuery); |
@@ -1,1 +0,1 @@ | ||
"use strict";!function(a){function n(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}a.fn.tagify=function(){var i=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};return this.each(function(){var t,e=a(this);if(e.data("tagify"))return this;i.isJQueryPlugin=!0,t=new n(e[0],i),e.data("tagify",t)})},n.prototype={isIE:window.document.documentMode,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,autoComplete:!0,mapValueToProp:"",dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},escapeHtml:function(t){var e=document.createTextNode(t),i=document.createElement("p");return i.appendChild(e),i.innerHTML},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';e.originalInput=t,e.scope=this.parseHTML(i),e.input=e.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(e.scope,t),0<=this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)a(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&a(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;t&&this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){this.DOM.input.value&&(t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest"))},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.toLowerCase()===t.value.toLowerCase()})},markTagByValue:function(t,e){var i;return e||(i=this.isTagDuplicate.call(this,t),e=this.DOM.scope.querySelectorAll("tag")[i]),!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&"value"in t[0],s=[];if(n)return t;if(!n){if(!t.trim())return[];t=t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}})}return!n&&e&&(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),t=s),t},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue.call(n,t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){var n,s=this.getNodeIndex(t);t&&(i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n))))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){var e=this,t=this.value.map(function(t){return t[e.settings.mapValueToProp||"value"]||t.value});this.DOM.originalInput.value=JSON.stringify(t)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),!i||e.length<2?this.dropdown.hide.call(this):(this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this))))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":this.input.autocomplete.set.call(this,e?e.textContent:null)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}}}(jQuery); | ||
"use strict";!function(a){function n(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}a.fn.tagify=function(){var i=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};return this.each(function(){var t,e=a(this);if(e.data("tagify"))return this;i.isJQueryPlugin=!0,t=new n(e[0],i),e.data("tagify",t)})},n.prototype={isIE:window.document.documentMode,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},escapeHtml:function(t){var e=document.createTextNode(t),i=document.createElement("p");return i.appendChild(e),i.innerHTML},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';e.originalInput=t,e.scope=this.parseHTML(i),e.input=e.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(e.scope,t),0<=this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)a(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&a(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;if(t){try{t=JSON.parse(t)}catch(t){}this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})}},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){t&&this.input.value?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.trim().toLowerCase()===t.value.toLowerCase()})},getTagIndexByValue:function(i){var n=[];return this.DOM.scope.querySelectorAll("tag").forEach(function(t,e){t.textContent.trim().toLowerCase()==i.toLowerCase()&&n.push(e)}),n},getTagElmByValue:function(t){var e=this.getTagIndexByValue(t)[0];return this.DOM.scope.querySelectorAll("tag")[e]},markTagByValue:function(t,e){return!!(e=e||this.getTagElmByValue(t))&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&t[0]instanceof Object&&"value"in t[0],s=[];return n?t:"string"==typeof t?t.trim()?t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}}):[]:t instanceof Array?t.map(function(t){return{value:t.trim()}}):e?(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),s):void 0},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue(t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){"string"==typeof t&&(t=this.getTagElmByValue(t));var n,s=this.getTagIndexByValue(t.textContent);i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n)))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=JSON.stringify(this.value)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this)))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":case"Tab":return t.preventDefault(),this.input.autocomplete.set.call(this,e?e.textContent:null),!1}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}}}(jQuery); |
1532
dist/tagify.js
/** | ||
* Tagify (v 2.1.8)- tags input component | ||
* Tagify (v 2.2.10)- tags input component | ||
* By Yair Even-Or (2016) | ||
@@ -16,949 +16,917 @@ * Don't sell this code. (c) | ||
}(this, function() { | ||
'use strict'; | ||
"use strict"; | ||
function Tagify(input, settings) { | ||
// protection | ||
if (!input) { | ||
console.warn('Tagify: ', 'invalid input element ', input); | ||
return this; | ||
} | ||
// protection | ||
if (!input) { | ||
console.warn('Tagify: ', 'invalid input element ', input); | ||
return this; | ||
} | ||
this.settings = this.extend({}, this.DEFAULTS, settings); | ||
this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component | ||
this.settings = this.extend({}, this.DEFAULTS, settings); | ||
this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component | ||
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false | ||
if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false | ||
if (input.pattern) try { | ||
this.settings.pattern = new RegExp(input.pattern); | ||
if (input.pattern) try { | ||
this.settings.pattern = new RegExp(input.pattern); | ||
} catch (e) {} // Convert the "delimiters" setting into a REGEX object | ||
if (this.settings && this.settings.delimiters) { | ||
try { | ||
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g"); | ||
} catch (e) {} | ||
} | ||
// Convert the "delimiters" setting into a REGEX object | ||
if (this.settings && this.settings.delimiters) { | ||
try { | ||
this.settings.delimiters = new RegExp("[" + this.settings.delimiters + "]", "g"); | ||
} catch (e) {} | ||
} | ||
this.value = []; // tags' data | ||
// events' callbacks references will be stores here, so events could be unbinded | ||
this.value = []; // tags' data | ||
this.listeners = {}; | ||
this.DOM = {}; // Store all relevant DOM elements in an Object | ||
// events' callbacks references will be stores here, so events could be unbinded | ||
this.listeners = {}; | ||
this.DOM = {}; // Store all relevant DOM elements in an Object | ||
this.extend(this, new this.EventDispatcher(this)); | ||
this.build(input); | ||
this.loadOriginalValues(); | ||
this.events.customBinding.call(this); | ||
this.events.binding.call(this); | ||
this.extend(this, new this.EventDispatcher(this)); | ||
this.build(input); | ||
this.loadOriginalValues(); | ||
this.events.customBinding.call(this); | ||
this.events.binding.call(this); | ||
} | ||
Tagify.prototype = { | ||
isIE: window.document.documentMode, | ||
isIE: window.document.documentMode, | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility | ||
TEXTS: { | ||
empty: "empty", | ||
exceed: "number of tags exceeded", | ||
pattern: "pattern mismatch", | ||
duplicate: "already exists", | ||
notAllowed: "not allowed" | ||
}, | ||
DEFAULTS: { | ||
delimiters: ",", | ||
// [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |." | ||
pattern: null, | ||
// RegEx pattern to validate input by. Ex: /[1-9]/ | ||
maxTags: Infinity, | ||
// Maximum number of tags | ||
callbacks: {}, | ||
// Exposed callbacks object to be triggered on certain events | ||
addTagOnBlur: true, | ||
// Flag - automatically adds the text which was inputed as a tag when blur event happens | ||
duplicates: false, | ||
// Flag - allow tuplicate tags | ||
whitelist: [], | ||
// Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting) | ||
blacklist: [], | ||
// A list of non-allowed tags | ||
enforceWhitelist: false, | ||
// Flag - Only allow tags allowed in whitelist | ||
keepInvalidTags: false, | ||
// Flag - if true, do not remove tags which did not pass validation | ||
autoComplete: true, | ||
// Flag - tries to autocomplete the input's value while typing | ||
dropdown: { | ||
classname: '', | ||
enabled: 2, | ||
// minimum input characters needs to be typed for the dropdown to show | ||
maxItems: 10, | ||
itemTemplate: '' | ||
} | ||
}, | ||
customEventsList: ['add', 'remove', 'invalid'], | ||
TEXTS: { | ||
empty: "empty", | ||
exceed: "number of tags exceeded", | ||
pattern: "pattern mismatch", | ||
duplicate: "already exists", | ||
notAllowed: "not allowed" | ||
}, | ||
/** | ||
* utility method | ||
* https://stackoverflow.com/a/35385518/104380 | ||
* @param {String} s [HTML string] | ||
* @return {Object} [DOM node] | ||
*/ | ||
parseHTML: function parseHTML(s) { | ||
var parser = new DOMParser(), | ||
node = parser.parseFromString(s.trim(), "text/html"); | ||
return node.body.firstElementChild; | ||
}, | ||
// https://stackoverflow.com/a/25396011/104380 | ||
escapeHtml: function escapeHtml(s) { | ||
var text = document.createTextNode(s), | ||
p = document.createElement('p'); | ||
p.appendChild(text); | ||
return p.innerHTML; | ||
}, | ||
DEFAULTS: { | ||
delimiters: ",", // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |." | ||
pattern: null, // RegEx pattern to validate input by. Ex: /[1-9]/ | ||
maxTags: Infinity, // Maximum number of tags | ||
callbacks: {}, // Exposed callbacks object to be triggered on certain events | ||
addTagOnBlur: true, // Flag - automatically adds the text which was inputed as a tag when blur event happens | ||
duplicates: false, // Flag - allow tuplicate tags | ||
whitelist: [], // Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting) | ||
blacklist: [], // A list of non-allowed tags | ||
enforceWhitelist: false, // Flag - Only allow tags allowed in whitelist | ||
keepInvalidTags: false, // Flag - if true, do not remove tags which did not pass validation | ||
autoComplete: true, // Flag - tries to autocomplete the input's value while typing | ||
mapValueToProp: "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value" | ||
dropdown: { | ||
classname: '', | ||
enabled: 2, // minimum input characters needs to be typed for the dropdown to show | ||
maxItems: 10, | ||
itemTemplate: '' | ||
} | ||
}, | ||
/** | ||
* builds the HTML of this component | ||
* @param {Object} input [DOM element which would be "transformed" into "Tags"] | ||
*/ | ||
build: function build(input) { | ||
var that = this, | ||
DOM = this.DOM, | ||
template = "<tags class=\"tagify " + input.className + "\" " + (this.settings.readonly ? 'readonly' : '') + ">\n <div contenteditable data-placeholder=\"" + input.placeholder + "\" class=\"tagify__input\"></div>\n </tags>"; | ||
DOM.originalInput = input; | ||
DOM.scope = this.parseHTML(template); | ||
DOM.input = DOM.scope.querySelector('[contenteditable]'); | ||
input.parentNode.insertBefore(DOM.scope, input); | ||
customEventsList: ['add', 'remove', 'invalid'], | ||
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) { | ||
this.dropdown.init.call(this); | ||
} | ||
/** | ||
* utility method | ||
* https://stackoverflow.com/a/35385518/104380 | ||
* @param {String} s [HTML string] | ||
* @return {Object} [DOM node] | ||
*/ | ||
parseHTML: function parseHTML(s) { | ||
var parser = new DOMParser(), | ||
node = parser.parseFromString(s.trim(), "text/html"); | ||
input.autofocus && DOM.input.focus(); | ||
}, | ||
return node.body.firstElementChild; | ||
}, | ||
/** | ||
* Reverts back any changes made by this component | ||
*/ | ||
destroy: function destroy() { | ||
this.DOM.scope.parentNode.removeChild(this.DOM.scope); | ||
}, | ||
/** | ||
* Merge two objects into a new one | ||
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}}) | ||
*/ | ||
extend: function extend(o, o1, o2) { | ||
if (!(o instanceof Object)) o = {}; | ||
copy(o, o1); | ||
if (o2) copy(o, o2); | ||
// https://stackoverflow.com/a/25396011/104380 | ||
escapeHtml: function escapeHtml(s) { | ||
var text = document.createTextNode(s), | ||
p = document.createElement('p'); | ||
p.appendChild(text); | ||
return p.innerHTML; | ||
}, | ||
function isObject(obj) { | ||
var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1); | ||
return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement'; | ||
} | ||
; | ||
/** | ||
* builds the HTML of this component | ||
* @param {Object} input [DOM element which would be "transformed" into "Tags"] | ||
*/ | ||
build: function build(input) { | ||
var that = this, | ||
DOM = this.DOM, | ||
template = '<tags class="tagify ' + input.className + '" ' + (this.settings.readonly ? 'readonly' : '') + '>\n <div contenteditable data-placeholder="' + input.placeholder + '" class="tagify__input"></div>\n </tags>'; | ||
DOM.originalInput = input; | ||
DOM.scope = this.parseHTML(template); | ||
DOM.input = DOM.scope.querySelector('[contenteditable]'); | ||
input.parentNode.insertBefore(DOM.scope, input); | ||
if (this.settings.dropdown.enabled >= 0 && this.settings.whitelist.length) { | ||
this.dropdown.init.call(this); | ||
function copy(a, b) { | ||
// copy o2 to o | ||
for (var key in b) { | ||
if (b.hasOwnProperty(key)) { | ||
if (isObject(b[key])) { | ||
if (!isObject(a[key])) { | ||
a[key] = Object.assign({}, b[key]); | ||
} else copy(a[key], b[key]); | ||
} else a[key] = b[key]; | ||
} | ||
} | ||
} | ||
input.autofocus && DOM.input.focus(); | ||
}, | ||
return o; | ||
}, | ||
/** | ||
* A constructor for exposing events to the outside | ||
*/ | ||
EventDispatcher: function EventDispatcher(instance) { | ||
// Create a DOM EventTarget object | ||
var target = document.createTextNode(''); // Pass EventTarget interface calls to DOM EventTarget object | ||
/** | ||
* Reverts back any changes made by this component | ||
*/ | ||
destroy: function destroy() { | ||
this.DOM.scope.parentNode.removeChild(this.DOM.scope); | ||
}, | ||
this.off = function (name, cb) { | ||
if (cb) target.removeEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
this.on = function (name, cb) { | ||
if (cb) target.addEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
/** | ||
* Merge two objects into a new one | ||
* TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}}) | ||
*/ | ||
extend: function extend(o, o1, o2) { | ||
if (!(o instanceof Object)) o = {}; | ||
this.trigger = function (eventName, data) { | ||
var e; | ||
if (!eventName) return; | ||
copy(o, o1); | ||
if (o2) copy(o, o2); | ||
if (instance.settings.isJQueryPlugin) { | ||
$(instance.DOM.originalInput).triggerHandler(eventName, [data]); | ||
} else { | ||
try { | ||
e = new CustomEvent(eventName, { | ||
"detail": data | ||
}); | ||
} catch (err) { | ||
console.warn(err); | ||
} | ||
function isObject(obj) { | ||
var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1); | ||
return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement'; | ||
}; | ||
target.dispatchEvent(e); | ||
} | ||
}; | ||
}, | ||
function copy(a, b) { | ||
// copy o2 to o | ||
for (var key in b) { | ||
if (b.hasOwnProperty(key)) { | ||
if (isObject(b[key])) { | ||
if (!isObject(a[key])) { | ||
a[key] = Object.assign({}, b[key]); | ||
} else copy(a[key], b[key]); | ||
} else a[key] = b[key]; | ||
} | ||
} | ||
} | ||
/** | ||
* DOM events listeners binding | ||
*/ | ||
events: { | ||
// bind custom events which were passed in the settings | ||
customBinding: function customBinding() { | ||
var _this2 = this; | ||
return o; | ||
this.customEventsList.forEach(function (name) { | ||
_this2.on(name, _this2.settings.callbacks[name]); | ||
}); | ||
}, | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
var _CB = this.events.callbacks, | ||
// setup callback references so events could be removed later | ||
_CBR = this.listeners.main = this.listeners.main || { | ||
paste: ['input', _CB.onPaste.bind(this)], | ||
focus: ['input', _CB.onFocusBlur.bind(this)], | ||
blur: ['input', _CB.onFocusBlur.bind(this)], | ||
keydown: ['input', _CB.onKeydown.bind(this)], | ||
click: ['scope', _CB.onClickScope.bind(this)] | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
/** | ||
* A constructor for exposing events to the outside | ||
*/ | ||
EventDispatcher: function EventDispatcher(instance) { | ||
// Create a DOM EventTarget object | ||
var target = document.createTextNode(''); | ||
for (var eventName in _CBR) { | ||
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]); | ||
} | ||
// Pass EventTarget interface calls to DOM EventTarget object | ||
this.off = function (name, cb) { | ||
if (cb) target.removeEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
this.on = function (name, cb) { | ||
if (cb) target.addEventListener.call(target, name, cb); | ||
return this; | ||
}; | ||
this.trigger = function (eventName, data) { | ||
var e; | ||
if (!eventName) return; | ||
if (instance.settings.isJQueryPlugin) { | ||
$(instance.DOM.originalInput).triggerHandler(eventName, [data]); | ||
} else { | ||
try { | ||
e = new CustomEvent(eventName, { "detail": data }); | ||
} catch (err) { | ||
console.warn(err); | ||
} | ||
target.dispatchEvent(e); | ||
} | ||
}; | ||
if (bindUnbind) { | ||
// this event should never be unbinded | ||
// IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead.. | ||
this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this)); | ||
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this)); | ||
} | ||
}, | ||
/** | ||
* DOM events listeners binding | ||
* DOM events callbacks | ||
*/ | ||
events: { | ||
// bind custom events which were passed in the settings | ||
customBinding: function customBinding() { | ||
var _this2 = this; | ||
callbacks: { | ||
onFocusBlur: function onFocusBlur(e) { | ||
var s = e.target.textContent.trim(); | ||
this.customEventsList.forEach(function (name) { | ||
_this2.on(name, _this2.settings.callbacks[name]); | ||
}); | ||
}, | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
if (e.type == "focus") { | ||
// e.target.classList.remove('placeholder'); | ||
if (this.settings.dropdown.enabled === 0) { | ||
this.dropdown.show.call(this); | ||
} | ||
} else if (e.type == "blur" && s) { | ||
this.settings.addTagOnBlur && this.addTags(s, true).length; | ||
} else { | ||
// e.target.classList.add('placeholder'); | ||
this.DOM.input.removeAttribute('style'); | ||
this.dropdown.hide.call(this); | ||
} | ||
}, | ||
onKeydown: function onKeydown(e) { | ||
var s = e.target.textContent, | ||
lastTag; | ||
var _CB = this.events.callbacks, | ||
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) { | ||
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)'); | ||
lastTag = lastTag[lastTag.length - 1]; | ||
this.removeTag(lastTag); | ||
} else if (e.key == 'Escape' || e.key == 'Esc') { | ||
this.input.set.call(this); | ||
e.target.blur(); | ||
} else if (e.key == 'Enter') { | ||
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 | ||
// setup callback references so events could be removed later | ||
_CBR = this.listeners.main = this.listeners.main || { | ||
paste: ['input', _CB.onPaste.bind(this)], | ||
focus: ['input', _CB.onFocusBlur.bind(this)], | ||
blur: ['input', _CB.onFocusBlur.bind(this)], | ||
keydown: ['input', _CB.onKeydown.bind(this)], | ||
click: ['scope', _CB.onClickScope.bind(this)] | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
this.addTags(this.input.value, true); | ||
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this); | ||
}, | ||
onInput: function onInput(e) { | ||
var value = this.input.normalize.call(this), | ||
showSuggestions = value.length >= this.settings.dropdown.enabled; | ||
for (var eventName in _CBR) { | ||
this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]); | ||
} | ||
if (!value) { | ||
this.input.set.call(this, ''); | ||
return; | ||
} | ||
if (bindUnbind) { | ||
// this event should never be unbinded | ||
// IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead.. | ||
this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this)); | ||
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead | ||
// save the value on the input's State object | ||
if (this.settings.isJQueryPlugin) $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this)); | ||
} | ||
}, | ||
this.input.set.call(this, value, false); | ||
if (value.search(this.settings.delimiters) != -1) { | ||
if (this.addTags(value).length) { | ||
this.input.set.call(this); // clear the input field's value | ||
} | ||
} else if (this.settings.dropdown.enabled >= 0) { | ||
this.dropdown[showSuggestions ? "show" : "hide"].call(this, value); | ||
} | ||
}, | ||
onInputIE: function onInputIE(e) { | ||
var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay | ||
/** | ||
* DOM events callbacks | ||
*/ | ||
callbacks: { | ||
onFocusBlur: function onFocusBlur(e) { | ||
var s = e.target.textContent.trim(); | ||
if (e.type == "focus") { | ||
// e.target.classList.remove('placeholder'); | ||
if (this.settings.dropdown.enabled === 0) { | ||
this.dropdown.show.call(this); | ||
} | ||
} else if (e.type == "blur" && s) { | ||
this.settings.addTagOnBlur && this.addTags(s, true).length; | ||
} else { | ||
// e.target.classList.add('placeholder'); | ||
this.DOM.input.removeAttribute('style'); | ||
this.dropdown.hide.call(this); | ||
} | ||
}, | ||
onKeydown: function onKeydown(e) { | ||
var s = e.target.textContent, | ||
lastTag; | ||
setTimeout(function () { | ||
_this.events.callbacks.onInput.call(_this, e); | ||
}); | ||
}, | ||
onPaste: function onPaste(e) {}, | ||
onClickScope: function onClickScope(e) { | ||
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") { | ||
this.removeTag(e.target.parentNode); | ||
} | ||
} | ||
} | ||
}, | ||
if (e.key == 'Backspace' && (s == "" || s.charCodeAt(0) == 8203)) { | ||
lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)'); | ||
lastTag = lastTag[lastTag.length - 1]; | ||
this.removeTag(lastTag); | ||
} else if (e.key == 'Escape' || e.key == 'Esc') { | ||
this.input.set.call(this); | ||
e.target.blur(); | ||
} else if (e.key == 'Enter') { | ||
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 | ||
this.addTags(this.input.value, true); | ||
} else if (e.key == 'ArrowRight') this.input.autocomplete.set.call(this); | ||
}, | ||
onInput: function onInput(e) { | ||
var value = this.input.normalize.call(this), | ||
showSuggestions = value.length >= this.settings.dropdown.enabled; | ||
/** | ||
* If the original input had an values, add them as tags | ||
*/ | ||
loadOriginalValues: function loadOriginalValues() { | ||
var value = this.DOM.originalInput.value; // if the original input already had any value (tags) | ||
if (!value) { | ||
this.input.set.call(this, ''); | ||
return; | ||
} | ||
if (!value) return; | ||
if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead | ||
try { | ||
value = JSON.parse(value); | ||
} catch (err) {} | ||
// save the value on the input's State object | ||
this.input.set.call(this, value, false); | ||
this.addTags(value).forEach(function (tag) { | ||
tag && tag.classList.add('tagify--noAnim'); | ||
}); | ||
}, | ||
if (value.search(this.settings.delimiters) != -1) { | ||
if (this.addTags(value).length) { | ||
this.input.set.call(this); // clear the input field's value | ||
} | ||
} else if (this.settings.dropdown.enabled >= 0) { | ||
this.dropdown[showSuggestions ? "show" : "hide"].call(this, value); | ||
} | ||
}, | ||
onInputIE: function onInputIE(e) { | ||
var _this = this; | ||
// for the "e.target.textContent" to be changed, the browser requires a small delay | ||
setTimeout(function () { | ||
_this.events.callbacks.onInput.call(_this, e); | ||
}); | ||
}, | ||
onPaste: function onPaste(e) {}, | ||
onClickScope: function onClickScope(e) { | ||
if (e.target.tagName == "TAGS") this.DOM.input.focus();else if (e.target.tagName == "X") { | ||
this.removeTag(e.target.parentNode); | ||
} | ||
} | ||
} | ||
/** | ||
* input bridge for accessing & setting | ||
* @type {Object} | ||
*/ | ||
input: { | ||
value: '', | ||
set: function set() { | ||
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
this.input.value = s; | ||
if (updateDOM) this.DOM.input.innerHTML = s; | ||
if (!s) this.dropdown.hide.call(this); | ||
if (s.length < 2) this.input.autocomplete.suggest.call(this, ''); | ||
this.input.validate.call(this); | ||
}, | ||
// https://stackoverflow.com/a/3866442/104380 | ||
setRangeAtEnd: function setRangeAtEnd() { | ||
var range, selection; | ||
if (!document.createRange) return; | ||
range = document.createRange(); | ||
range.selectNodeContents(this.DOM.input); | ||
range.collapse(false); | ||
selection = window.getSelection(); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
}, | ||
/** | ||
* If the original input had an values, add them as tags | ||
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()" | ||
*/ | ||
loadOriginalValues: function loadOriginalValues() { | ||
var value = this.DOM.originalInput.value, | ||
values; | ||
validate: function validate() { | ||
var isValid = !this.input.value || this.validateTag.call(this, this.input.value); | ||
this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true); | ||
}, | ||
// remove any child DOM elements that aren't of type TEXT (like <br>) | ||
normalize: function normalize() { | ||
var clone = this.DOM.input.cloneNode(true), | ||
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters | ||
// if the original input already had any value (tags) | ||
if (!value) return; | ||
while (clone.firstElementChild) { | ||
v += clone.firstElementChild.textContent; | ||
clone.removeChild(clone.firstElementChild); | ||
} | ||
this.addTags(value).forEach(function (tag) { | ||
tag && tag.classList.add('tagify--noAnim'); | ||
}); | ||
return v.replace(/^\s+/, ""); // trimLeft | ||
}, | ||
/** | ||
* input bridge for accessing & setting | ||
* @type {Object} | ||
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)") | ||
* @param {String} s [description] | ||
*/ | ||
input: { | ||
value: '', | ||
set: function set() { | ||
var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
autocomplete: { | ||
suggest: function suggest(s) { | ||
if (!s || !this.input.value) this.DOM.input.removeAttribute("data-suggest");else this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length)); | ||
}, | ||
set: function set(s) { | ||
var dataSuggest = this.DOM.input.getAttribute('data-suggest'), | ||
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null); | ||
this.input.value = s; | ||
if (suggestion) { | ||
this.input.set.call(this, suggestion); | ||
this.input.autocomplete.suggest.call(this, ''); | ||
this.dropdown.hide.call(this); | ||
this.input.setRangeAtEnd.call(this); | ||
} // if( suggestion && this.addTags(this.input.value + suggestion).length ){ | ||
// this.input.set.call(this); | ||
// this.dropdown.hide.call(this); | ||
// } | ||
if (updateDOM) this.DOM.input.innerHTML = s; | ||
if (!s) this.dropdown.hide.call(this); | ||
if (s.length < 2) this.input.autocomplete.suggest.call(this, ''); | ||
} | ||
} | ||
}, | ||
getNodeIndex: function getNodeIndex(node) { | ||
var index = 0; | ||
this.input.validate.call(this); | ||
}, | ||
while (node = node.previousSibling) { | ||
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++; | ||
} | ||
return index; | ||
}, | ||
// https://stackoverflow.com/a/3866442/104380 | ||
setRangeAtEnd: function setRangeAtEnd() { | ||
var range, selection; | ||
/** | ||
* Searches if any tag with a certain value already exis | ||
* @param {String} s [text value to search for] | ||
* @return {int} [Position index of the tag. -1 is returned if tag is not found.] | ||
*/ | ||
isTagDuplicate: function isTagDuplicate(s) { | ||
return this.value.findIndex(function (item) { | ||
return s.trim().toLowerCase() === item.value.toLowerCase(); | ||
}); // return this.value.some(item => s.toLowerCase() === item.value.toLowerCase()); | ||
}, | ||
getTagIndexByValue: function getTagIndexByValue(value) { | ||
var result = []; | ||
this.DOM.scope.querySelectorAll('tag').forEach(function (tagElm, i) { | ||
if (tagElm.textContent.trim().toLowerCase() == value.toLowerCase()) result.push(i); | ||
}); | ||
return result; | ||
}, | ||
getTagElmByValue: function getTagElmByValue(value) { | ||
var tagIdx = this.getTagIndexByValue(value)[0]; | ||
return this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
}, | ||
if (!document.createRange) return; | ||
/** | ||
* Mark a tag element by its value | ||
* @param {String / Number} value [text value to search for] | ||
* @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings] | ||
* @return {boolean} [found / not found] | ||
*/ | ||
markTagByValue: function markTagByValue(value, tagElm) { | ||
var tagsElms, tagsElmsLen; | ||
tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined | ||
range = document.createRange(); | ||
range.selectNodeContents(this.DOM.input); | ||
range.collapse(false); | ||
selection = window.getSelection(); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
}, | ||
if (tagElm) { | ||
tagElm.classList.add('tagify--mark'); | ||
setTimeout(function () { | ||
tagElm.classList.remove('tagify--mark'); | ||
}, 100); | ||
return tagElm; | ||
} | ||
return false; | ||
}, | ||
/** | ||
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()" | ||
*/ | ||
validate: function validate() { | ||
var isValid = !this.input.value || this.validateTag.call(this, this.input.value); | ||
this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true); | ||
}, | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagBlacklisted: function isTagBlacklisted(v) { | ||
v = v.split(' '); | ||
return this.settings.blacklist.filter(function (x) { | ||
return v.indexOf(x) != -1; | ||
}).length; | ||
}, | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagWhitelisted: function isTagWhitelisted(v) { | ||
return this.settings.whitelist.some(function (item) { | ||
var value = item.value ? item.value : item; | ||
if (value.toLowerCase() === v.toLowerCase()) return true; | ||
}); | ||
}, | ||
// remove any child DOM elements that aren't of type TEXT (like <br>) | ||
normalize: function normalize() { | ||
var clone = this.DOM.input.cloneNode(true), | ||
v = clone.textContent.replace(/\s/g, ' '); // replace NBSPs with spaces characters | ||
/** | ||
* validate a tag object BEFORE the actual tag will be created & appeneded | ||
* @param {String} s | ||
* @return {Boolean/String} ["true" if validation has passed, String for a fail] | ||
*/ | ||
validateTag: function validateTag(s) { | ||
var value = s.trim(), | ||
maxTagsExceed = this.value.length >= this.settings.maxTags, | ||
isDuplicate, | ||
eventName__error, | ||
result = true; // check for empty value | ||
while (clone.firstElementChild) { | ||
v += clone.firstElementChild.textContent; | ||
clone.removeChild(clone.firstElementChild); | ||
} | ||
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; // check if pattern should be used and if so, use it to test the value | ||
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate | ||
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed; | ||
return result; | ||
}, | ||
return v.replace(/^\s+/, ""); // trimLeft | ||
}, | ||
/** | ||
* pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words | ||
* so each item should be iterated on and a tag created for. | ||
* @return {Array} [Array of Objects] | ||
*/ | ||
normalizeTags: function normalizeTags(tagsItems) { | ||
var _this3 = this; | ||
var whitelistWithProps = this.settings.whitelist[0] instanceof Object, | ||
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0], | ||
// checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
temp = []; // no need to continue if "tagsItems" is an Array of Objects | ||
/** | ||
* suggest the rest of the input's value | ||
* @param {String} s [description] | ||
*/ | ||
autocomplete: { | ||
suggest: function suggest(s) { | ||
if (!this.DOM.input.value) return; // do not suggest anything for empty input | ||
if (s) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length));else this.DOM.input.removeAttribute("data-suggest"); | ||
}, | ||
set: function set(s) { | ||
var dataSuggest = this.DOM.input.getAttribute('data-suggest'), | ||
suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null); | ||
if (isComplex) return tagsItems; // if the value is a "simple" String, ex: "aaa, bbb, ccc" | ||
if (suggestion) { | ||
this.input.set.call(this, suggestion); | ||
this.input.autocomplete.suggest.call(this, ''); | ||
this.dropdown.hide.call(this); | ||
this.input.setRangeAtEnd.call(this); | ||
} | ||
if (typeof tagsItems == 'string') { | ||
if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones) | ||
// if( suggestion && this.addTags(this.input.value + suggestion).length ){ | ||
// this.input.set.call(this); | ||
// this.dropdown.hide.call(this); | ||
// } | ||
} | ||
} | ||
}, | ||
return tagsItems.split(this.settings.delimiters).filter(function (n) { | ||
return n; | ||
}).map(function (v) { | ||
return { | ||
value: v.trim() | ||
}; | ||
}); | ||
} | ||
getNodeIndex: function getNodeIndex(node) { | ||
var index = 0; | ||
while (node = node.previousSibling) { | ||
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) index++; | ||
}return index; | ||
}, | ||
if (tagsItems instanceof Array) return tagsItems.map(function (v) { | ||
return { | ||
value: v.trim() | ||
}; | ||
}); // search if the tag exists in the whitelist as an Object (has props), to be able to use its properties | ||
/** | ||
* Searches if any tag with a certain value already exis | ||
* @param {String} s [text value to search for] | ||
* @return {int} [Position index of the tag. -1 is returned if tag is not found.] | ||
*/ | ||
isTagDuplicate: function isTagDuplicate(s) { | ||
return this.value.findIndex(function (item) { | ||
return s.toLowerCase() === item.value.toLowerCase(); | ||
if (whitelistWithProps) { | ||
tagsItems.forEach(function (tag) { | ||
var matchObj = _this3.settings.whitelist.filter(function (WL_item) { | ||
return WL_item.value.toLowerCase() == tag.value.toLowerCase(); | ||
}); | ||
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase()); | ||
}, | ||
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value | ||
else temp.push(tag); | ||
}); | ||
return temp; | ||
} | ||
}, | ||
/** | ||
* Mark a tag element by its value | ||
* @param {String / Number} value [text value to search for] | ||
* @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings] | ||
* @return {boolean} [found / not found] | ||
*/ | ||
markTagByValue: function markTagByValue(value, tagElm) { | ||
var tagsElms, tagsElmsLen, tagIdx; | ||
/** | ||
* add a "tag" element to the "tags" component | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] | ||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] | ||
* @return {Array} Array of DOM elements (tags) | ||
*/ | ||
addTags: function addTags(tagsItems, clearInput) { | ||
var _this4 = this; | ||
if (!tagElm) { | ||
tagIdx = this.isTagDuplicate.call(this, value); | ||
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
} | ||
var tagElems = []; | ||
this.DOM.input.removeAttribute('style'); | ||
tagsItems = this.normalizeTags.call(this, tagsItems); | ||
tagsItems.forEach(function (tagData) { | ||
var tagValidation, tagElm; | ||
// check AGAIN if "tagElm" is defined | ||
if (tagElm) { | ||
tagElm.classList.add('tagify--mark'); | ||
setTimeout(function () { | ||
tagElm.classList.remove('tagify--mark'); | ||
}, 100); | ||
return tagElm; | ||
} | ||
if (typeof _this4.settings.transformTag === 'function') { | ||
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value; | ||
} | ||
return false; | ||
}, | ||
tagValidation = _this4.validateTag.call(_this4, tagData.value); | ||
if (tagValidation !== true) { | ||
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed"; | ||
tagData.title = tagValidation; | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagBlacklisted: function isTagBlacklisted(v) { | ||
v = v.split(' '); | ||
return this.settings.blacklist.filter(function (x) { | ||
return v.indexOf(x) != -1; | ||
}).length; | ||
}, | ||
_this4.markTagByValue(tagData.value); | ||
/** | ||
* make sure the tag, or words in it, is not in the blacklist | ||
*/ | ||
isTagWhitelisted: function isTagWhitelisted(v) { | ||
return this.settings.whitelist.some(function (item) { | ||
var value = item.value ? item.value : item; | ||
if (value.toLowerCase() === v.toLowerCase()) return true; | ||
_this4.trigger("invalid", { | ||
value: tagData.value, | ||
index: _this4.value.length, | ||
message: tagValidation | ||
}); | ||
}, | ||
} // Create tag HTML element | ||
/** | ||
* validate a tag object BEFORE the actual tag will be created & appeneded | ||
* @param {String} s | ||
* @return {Boolean/String} ["true" if validation has passed, String for a fail] | ||
*/ | ||
validateTag: function validateTag(s) { | ||
var value = s.trim(), | ||
maxTagsExceed = this.value.length >= this.settings.maxTags, | ||
isDuplicate, | ||
eventName__error, | ||
result = true; | ||
tagElm = _this4.createTagElem(tagData); | ||
tagElems.push(tagElm); // add the tag to the component's DOM | ||
// check for empty value | ||
if (!value) result = this.TEXTS.empty;else if (maxTagsExceed) result = this.TEXTS.exceed; | ||
appendTag.call(_this4, tagElm); | ||
// check if pattern should be used and if so, use it to test the value | ||
else if (this.settings.pattern && !this.settings.pattern.test(value)) result = this.TEXTS.pattern; | ||
if (tagValidation === true) { | ||
// update state | ||
_this4.value.push(tagData); | ||
// if duplicates are not allowed and there is a duplicate | ||
else if (!this.settings.duplicates && this.isTagDuplicate(value) !== -1) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || this.settings.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed; | ||
_this4.update(); | ||
return result; | ||
}, | ||
_this4.trigger('add', _this4.extend({}, { | ||
index: _this4.value.length, | ||
tag: tagElm | ||
}, tagData)); | ||
} else if (!_this4.settings.keepInvalidTags) { | ||
// remove invalid tags (if "keepInvalidTags" is set to "false") | ||
setTimeout(function () { | ||
_this4.removeTag(tagElm, true); | ||
}, 1000); | ||
} | ||
}); | ||
if (tagsItems.length && clearInput) { | ||
this.input.set.call(this); | ||
} | ||
/** | ||
* pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words | ||
* so each item should be iterated on and a tag created for. | ||
* @return {Array} [Array of Objects] | ||
* appened (validated) tag to the component's DOM scope | ||
* @return {[type]} [description] | ||
*/ | ||
normalizeTags: function normalizeTags(tagsItems) { | ||
var _this3 = this; | ||
var whitelistWithProps = this.settings.whitelist[0] instanceof Object, | ||
isComplex = tagsItems instanceof Array && "value" in tagsItems[0], | ||
// checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
temp = []; | ||
// no need to continue if "tagsItems" is an Array of Objects | ||
if (isComplex) return tagsItems; | ||
function appendTag(tagElm) { | ||
var insertBeforeNode = this.DOM.scope.lastElementChild; | ||
if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm); | ||
} | ||
// if the value is a "simple" String, ex: "aaa, bbb, ccc" | ||
if (!isComplex) { | ||
if (!tagsItems.trim()) return []; | ||
return tagElems; | ||
}, | ||
// go over each tag and add it (if there were multiple ones) | ||
tagsItems = tagsItems.split(this.settings.delimiters).filter(function (n) { | ||
return n; | ||
}).map(function (v) { | ||
return { value: v.trim() }; | ||
}); | ||
} | ||
/** | ||
* creates a DOM tag element and injects it into the component (this.DOM.scope) | ||
* @param Object} tagData [text value & properties for the created tag] | ||
* @return {Object} [DOM element] | ||
*/ | ||
createTagElem: function createTagElem(tagData) { | ||
var tagElm, | ||
v = this.escapeHtml(tagData.value), | ||
template = "<tag title='" + v + "'>\n <x title=''></x><div><span>" + v + "</span></div>\n </tag>"; | ||
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties | ||
if (!isComplex && whitelistWithProps) { | ||
tagsItems.forEach(function (tag) { | ||
var matchObj = _this3.settings.whitelist.filter(function (WL_item) { | ||
return WL_item.value.toLowerCase() == tag.value.toLowerCase(); | ||
}); | ||
if (matchObj[0]) temp.push(matchObj[0]); // set the Array (with the found Object) as the new value | ||
else temp.push(tag); | ||
}); | ||
if (typeof this.settings.tagTemplate === "function") { | ||
try { | ||
template = this.settings.tagTemplate(v, tagData); | ||
} catch (err) {} | ||
} // for a certain Tag element, add attributes. | ||
tagsItems = temp; | ||
} | ||
return tagsItems; | ||
}, | ||
function addTagAttrs(tagElm, tagData) { | ||
var i, | ||
keys = Object.keys(tagData); | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (!tagData.hasOwnProperty(propName)) return; | ||
tagElm.setAttribute(propName, tagData[propName]); | ||
} | ||
} | ||
/** | ||
* add a "tag" element to the "tags" component | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects] | ||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] | ||
* @return {Array} Array of DOM elements (tags) | ||
*/ | ||
addTags: function addTags(tagsItems, clearInput) { | ||
var _this4 = this; | ||
tagElm = this.parseHTML(template); // add any attribuets, if exists | ||
var tagElems = []; | ||
addTagAttrs(tagElm, tagData); | ||
return tagElm; | ||
}, | ||
this.DOM.input.removeAttribute('style'); | ||
/** | ||
* Removes a tag | ||
* @param {Object|String} tagElm [DOM element or a String value] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
*/ | ||
removeTag: function removeTag(tagElm, silent) { | ||
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250; | ||
if (!tagElm) return; | ||
if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm); | ||
var tagData, | ||
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable) | ||
tagsItems = this.normalizeTags.call(this, tagsItems); | ||
if (tranDuration && tranDuration > 10) animation();else removeNode(); | ||
tagsItems.forEach(function (tagData) { | ||
var tagValidation, tagElm; | ||
if (!silent) { | ||
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object | ||
if (typeof _this4.settings.transformTag === 'function') { | ||
tagData.value = _this4.settings.transformTag.call(_this4, tagData.value) || tagData.value; | ||
} | ||
this.update(); // update the original input with the current value | ||
tagValidation = _this4.validateTag.call(_this4, tagData.value); | ||
this.trigger('remove', this.extend({}, { | ||
index: tagIdx, | ||
tag: tagElm | ||
}, tagData)); | ||
} | ||
if (tagValidation !== true) { | ||
tagData.class = tagData.class ? tagData.class + " tagify--notAllowed" : "tagify--notAllowed"; | ||
tagData.title = tagValidation; | ||
_this4.markTagByValue.call(_this4, tagData.value); | ||
_this4.trigger("invalid", { value: tagData.value, index: _this4.value.length, message: tagValidation }); | ||
} | ||
function animation() { | ||
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px'; | ||
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below | ||
// Create tag HTML element | ||
tagElm = _this4.createTagElem(tagData); | ||
tagElems.push(tagElm); | ||
tagElm.classList.add('tagify--hide'); // manual timeout (hack, since transitionend cannot be used because of hover) | ||
// add the tag to the component's DOM | ||
appendTag.call(_this4, tagElm); | ||
setTimeout(removeNode, 400); | ||
} | ||
if (tagValidation === true) { | ||
// update state | ||
_this4.value.push(tagData); | ||
_this4.update(); | ||
_this4.trigger('add', _this4.extend({}, { index: _this4.value.length, tag: tagElm }, tagData)); | ||
} else if (!_this4.settings.keepInvalidTags) { | ||
// remove invalid tags (if "keepInvalidTags" is set to "false") | ||
setTimeout(function () { | ||
_this4.removeTag(tagElm, true); | ||
}, 1000); | ||
} | ||
}); | ||
function removeNode() { | ||
tagElm.parentNode.removeChild(tagElm); | ||
} | ||
}, | ||
removeAllTags: function removeAllTags() { | ||
this.value = []; | ||
this.update(); | ||
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) { | ||
return elm.parentNode.removeChild(elm); | ||
}); | ||
}, | ||
if (tagsItems.length && clearInput) { | ||
this.input.set.call(this); | ||
} | ||
/** | ||
* update the origianl (hidden) input field's value | ||
* see - https://stackoverflow.com/q/50957841/104380 | ||
*/ | ||
update: function update() { | ||
this.DOM.originalInput.value = JSON.stringify(this.value); | ||
}, | ||
/** | ||
* appened (validated) tag to the component's DOM scope | ||
* @return {[type]} [description] | ||
*/ | ||
function appendTag(tagElm) { | ||
var insertBeforeNode = this.DOM.scope.lastElementChild; | ||
if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm); | ||
} | ||
return tagElems; | ||
/** | ||
* Dropdown controller | ||
* @type {Object} | ||
*/ | ||
dropdown: { | ||
init: function init() { | ||
this.DOM.dropdown = this.dropdown.build.call(this); | ||
}, | ||
build: function build() { | ||
var className = ("tagify__dropdown " + this.settings.dropdown.classname).trim(), | ||
template = "<div class=\"" + className + "\"></div>"; | ||
return this.parseHTML(template); | ||
}, | ||
show: function show(value) { | ||
var listItems, listHTML; | ||
if (!this.settings.whitelist.length) return; // if no value was supplied, show all the "whitelist" items in the dropdown | ||
// @type [Array] listItems | ||
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0); | ||
listHTML = this.dropdown.createListHTML.call(this, listItems); // set the first item from the suggestions list as the autocomplete value | ||
/** | ||
* creates a DOM tag element and injects it into the component (this.DOM.scope) | ||
* @param Object} tagData [text value & properties for the created tag] | ||
* @return {Object} [DOM element] | ||
*/ | ||
createTagElem: function createTagElem(tagData) { | ||
var tagElm, | ||
v = this.escapeHtml(tagData.value), | ||
template = '<tag title=\'' + v + '\'>\n <x title=\'\'></x><div><span>' + v + '</span></div>\n </tag>'; | ||
if (this.settings.autoComplete) { | ||
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : ''); | ||
} // if( !listHTML || listItems.length < 2 ){ | ||
// this.dropdown.hide.call(this); | ||
// return; | ||
// } | ||
if (typeof this.settings.tagTemplate === "function") { | ||
try { | ||
template = this.settings.tagTemplate(v, tagData); | ||
} catch (err) {} | ||
} | ||
// for a certain Tag element, add attributes. | ||
function addTagAttrs(tagElm, tagData) { | ||
var i, | ||
keys = Object.keys(tagData); | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (!tagData.hasOwnProperty(propName)) return; | ||
tagElm.setAttribute(propName, tagData[propName]); | ||
} | ||
} | ||
this.DOM.dropdown.innerHTML = listHTML; | ||
this.dropdown.position.call(this); // if the dropdown has yet to be appended to the document, | ||
// append the dropdown to the body element & handle events | ||
tagElm = this.parseHTML(template); | ||
if (!this.DOM.dropdown.parentNode != document.body) { | ||
document.body.appendChild(this.DOM.dropdown); | ||
this.events.binding.call(this, false); // unbind the main events | ||
// add any attribuets, if exists | ||
addTagAttrs(tagElm, tagData); | ||
return tagElm; | ||
this.dropdown.events.binding.call(this); | ||
} | ||
}, | ||
hide: function hide() { | ||
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return; | ||
document.body.removeChild(this.DOM.dropdown); | ||
window.removeEventListener('resize', this.dropdown.position); | ||
this.dropdown.events.binding.call(this, false); // unbind all events | ||
/** | ||
* Removes a tag | ||
* @param {Object} tagElm [DOM element] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
*/ | ||
removeTag: function removeTag(tagElm, silent) { | ||
var tranDuration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 250; | ||
if (!tagElm) return; | ||
var tagData, | ||
tagIdx = this.getNodeIndex(tagElm); | ||
if (!tagElm) return; | ||
if (tranDuration && tranDuration > 10) animation();else removeNode(); | ||
if (!silent) { | ||
tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object | ||
this.update(); // update the original input with the current value | ||
this.trigger('remove', this.extend({}, { index: tagIdx, tag: tagElm }, tagData)); | ||
} | ||
function animation() { | ||
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px'; | ||
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below | ||
tagElm.classList.add('tagify--hide'); | ||
// manual timeout (hack, since transitionend cannot be used because of hover) | ||
setTimeout(removeNode, 400); | ||
} | ||
function removeNode() { | ||
tagElm.parentNode.removeChild(tagElm); | ||
} | ||
this.events.binding.call(this); // re-bind main events | ||
}, | ||
removeAllTags: function removeAllTags() { | ||
this.value = []; | ||
this.update(); | ||
Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function (elm) { | ||
return elm.parentNode.removeChild(elm); | ||
}); | ||
position: function position() { | ||
var rect = this.DOM.scope.getBoundingClientRect(); | ||
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \ | ||
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \ | ||
width: " + rect.width + "px"; | ||
}, | ||
/** | ||
* update the origianl (hidden) input field's value | ||
* see - https://stackoverflow.com/q/50957841/104380 | ||
* @type {Object} | ||
*/ | ||
update: function update() { | ||
var _this5 = this; | ||
events: { | ||
/** | ||
* Events should only be binded when the dropdown is rendered and removed when isn't | ||
* @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] | ||
* @return {[type]} [description] | ||
*/ | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
var tagsAsString = this.value.map(function (v) { | ||
return v[_this5.settings.mapValueToProp || "value"] || v.value; | ||
}); | ||
this.DOM.originalInput.value = JSON.stringify(tagsAsString); | ||
}, | ||
/** | ||
* Dropdown controller | ||
* @type {Object} | ||
*/ | ||
dropdown: { | ||
init: function init() { | ||
this.DOM.dropdown = this.dropdown.build.call(this); | ||
// references to the ".bind()" methods must be saved so they could be unbinded later | ||
var _CBR = this.listeners.dropdown = this.listeners.dropdown || { | ||
position: this.dropdown.position.bind(this), | ||
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this), | ||
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this), | ||
onClick: this.dropdown.events.callbacks.onClick.bind(this) | ||
}, | ||
build: function build() { | ||
var className = ('tagify__dropdown ' + this.settings.dropdown.classname).trim(), | ||
template = '<div class="' + className + '"></div>'; | ||
return this.parseHTML(template); | ||
}, | ||
show: function show(value) { | ||
var listItems, listHTML; | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
if (!this.settings.whitelist.length) return; | ||
window[action]('resize', _CBR.position); | ||
window[action]('keydown', _CBR.onKeyDown); | ||
window[action]('mousedown', _CBR.onClick); | ||
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); // this.DOM.dropdown[action]('click', _CBR.onClick); | ||
}, | ||
callbacks: { | ||
onKeyDown: function onKeyDown(e) { | ||
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0], | ||
newValue = ""; | ||
listItems = value ? this.dropdown.filterListItems.call(this, value) : this.settings.whitelist.slice(0); | ||
switch (e.key) { | ||
case 'ArrowDown': | ||
case 'ArrowUp': | ||
case 'Down': // >IE11 | ||
listHTML = this.dropdown.createListHTML.call(this, listItems); | ||
case 'Up': | ||
// >IE11 | ||
e.preventDefault(); | ||
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop | ||
// set the first item from the suggestions list as the autocomplete value | ||
if (this.settings.autoComplete) { | ||
this.input.autocomplete.suggest.call(this, listItems.length ? listItems[0].value : ''); | ||
} | ||
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0]; | ||
this.dropdown.highlightOption.call(this, selectedElm, true); | ||
break; | ||
if (!listHTML || listItems.length < 2) { | ||
this.dropdown.hide.call(this); | ||
return; | ||
} | ||
case 'Escape': | ||
case 'Esc': | ||
// IE11 | ||
this.dropdown.hide.call(this); | ||
break; | ||
this.DOM.dropdown.innerHTML = listHTML; | ||
this.dropdown.position.call(this); | ||
case 'Enter': | ||
e.preventDefault(); | ||
newValue = selectedElm ? selectedElm.textContent : this.input.value; | ||
this.addTags(newValue, true); | ||
this.dropdown.hide.call(this); | ||
break; | ||
// if the dropdown has yet to be appended to the document, | ||
// append the dropdown to the body element & handle events | ||
if (!this.DOM.dropdown.parentNode != document.body) { | ||
document.body.appendChild(this.DOM.dropdown); | ||
this.events.binding.call(this, false); // unbind the main events | ||
this.dropdown.events.binding.call(this); | ||
} | ||
case 'ArrowRight': | ||
case 'Tab': | ||
e.preventDefault(); | ||
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null); | ||
return false; | ||
} | ||
}, | ||
hide: function hide() { | ||
if (!this.DOM.dropdown || this.DOM.dropdown.parentNode != document.body) return; | ||
document.body.removeChild(this.DOM.dropdown); | ||
window.removeEventListener('resize', this.dropdown.position); | ||
this.dropdown.events.binding.call(this, false); // unbind all events | ||
this.events.binding.call(this); // re-bind main events | ||
onMouseOver: function onMouseOver(e) { | ||
// event delegation check | ||
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target); | ||
}, | ||
position: function position() { | ||
var rect = this.DOM.scope.getBoundingClientRect(); | ||
onClick: function onClick(e) { | ||
var _this5 = this; | ||
this.DOM.dropdown.style.cssText = "left: " + (rect.left + window.pageXOffset) + "px; \ | ||
top: " + (rect.top + rect.height - 1 + window.pageYOffset) + "px; \ | ||
width: " + rect.width + "px"; | ||
}, | ||
var onClickOutside = function onClickOutside() { | ||
return _this5.dropdown.hide.call(_this5); | ||
}, | ||
listItemElm; | ||
if (e.button != 0) return; // allow only mouse left-clicks | ||
/** | ||
* @type {Object} | ||
*/ | ||
events: { | ||
if (e.target == document.documentElement) return onClickOutside(); | ||
listItemElm = [e.target, e.target.parentNode].filter(function (a) { | ||
return a.className.includes("tagify__dropdown__item"); | ||
})[0]; | ||
/** | ||
* Events should only be binded when the dropdown is rendered and removed when isn't | ||
* @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] | ||
* @return {[type]} [description] | ||
*/ | ||
binding: function binding() { | ||
var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; | ||
if (listItemElm) { | ||
this.input.set.call(this); | ||
this.addTags(listItemElm.textContent); | ||
} // clicked outside the dropdown, so just close it | ||
else onClickOutside(); | ||
} | ||
} | ||
}, | ||
highlightOption: function highlightOption(elm, adjustScroll) { | ||
if (!elm) return; | ||
var className = "tagify__dropdown__item--active"; // for IE support, which doesn't allow "forEach" on "NodeList" Objects | ||
// references to the ".bind()" methods must be saved so they could be unbinded later | ||
var _CBR = this.listeners.dropdown = this.listeners.dropdown || { | ||
position: this.dropdown.position.bind(this), | ||
onKeyDown: this.dropdown.events.callbacks.onKeyDown.bind(this), | ||
onMouseOver: this.dropdown.events.callbacks.onMouseOver.bind(this), | ||
onClick: this.dropdown.events.callbacks.onClick.bind(this) | ||
}, | ||
action = bindUnbind ? 'addEventListener' : 'removeEventListener'; | ||
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) { | ||
return activeElm.classList.remove(className); | ||
}); // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className)); | ||
window[action]('resize', _CBR.position); | ||
window[action]('keydown', _CBR.onKeyDown); | ||
window[action]('mousedown', _CBR.onClick); | ||
elm.classList.add(className); | ||
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; | ||
}, | ||
this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); | ||
// this.DOM.dropdown[action]('click', _CBR.onClick); | ||
}, | ||
/** | ||
* returns an HTML string of the suggestions' list items | ||
* @return {[type]} [description] | ||
*/ | ||
filterListItems: function filterListItems(value) { | ||
if (!value) return ""; | ||
var list = [], | ||
whitelist = this.settings.whitelist, | ||
suggestionsCount = this.settings.dropdown.maxItems || Infinity, | ||
whitelistItem, | ||
valueIsInWhitelist, | ||
i = 0; | ||
for (; i < whitelist.length; i++) { | ||
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { | ||
value: whitelist[i] | ||
}, //normalize value as an Object | ||
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">=" | ||
// match for the value within each "whitelist" item | ||
callbacks: { | ||
onKeyDown: function onKeyDown(e) { | ||
var selectedElm = this.DOM.dropdown.querySelectorAll("[class$='--active']")[0], | ||
newValue = ""; | ||
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem); | ||
if (suggestionsCount == 0) break; | ||
} | ||
switch (e.key) { | ||
case 'ArrowDown': | ||
case 'ArrowUp': | ||
case 'Down': // >IE11 | ||
case 'Up': | ||
// >IE11 | ||
e.preventDefault(); | ||
if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; | ||
return list; | ||
}, | ||
// if no element was found, loop | ||
if (!selectedElm) selectedElm = this.DOM.dropdown.children[e.key == 'ArrowUp' || e.key == 'Up' ? this.DOM.dropdown.children.length - 1 : 0]; | ||
/** | ||
* Creates the dropdown items' HTML | ||
* @param {Array} list [Array of Objects] | ||
* @return {String} | ||
*/ | ||
createListHTML: function createListHTML(list) { | ||
var getItem = this.settings.dropdown.itemTemplate || function (item) { | ||
return "<div class='tagify__dropdown__item " + (item.class ? item.class : "") + "' " + getAttributesString(item) + ">" + item.value + "</div>"; | ||
}; // for a certain Tag element, add attributes. | ||
this.dropdown.highlightOption.call(this, selectedElm, true); | ||
break; | ||
case 'Escape': | ||
case 'Esc': | ||
// IE11 | ||
this.dropdown.hide.call(this); | ||
break; | ||
function getAttributesString(item) { | ||
var i, | ||
keys = Object.keys(item), | ||
s = ""; | ||
case 'Enter': | ||
e.preventDefault(); | ||
newValue = selectedElm ? selectedElm.textContent : this.input.value; | ||
this.addTags(newValue, true); | ||
this.dropdown.hide.call(this); | ||
break; | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (propName != 'class' && !item.hasOwnProperty(propName)) return; | ||
s += " " + propName + (item[propName] ? "=" + item[propName] : ""); | ||
} | ||
case 'ArrowRight': | ||
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null); | ||
break; | ||
} | ||
}, | ||
onMouseOver: function onMouseOver(e) { | ||
// event delegation check | ||
if (e.target.className.includes('__item')) this.dropdown.highlightOption.call(this, e.target); | ||
}, | ||
onClick: function onClick(e) { | ||
var _this6 = this; | ||
return s; | ||
} | ||
var onClickOutside = function onClickOutside() { | ||
return _this6.dropdown.hide.call(_this6); | ||
}, | ||
listItemElm; | ||
if (e.button != 0) return; // allow only mouse left-clicks | ||
if (e.target == document.documentElement) return onClickOutside(); | ||
listItemElm = [e.target, e.target.parentNode].filter(function (a) { | ||
return a.className.includes("tagify__dropdown__item"); | ||
})[0]; | ||
if (listItemElm) { | ||
this.input.set.call(this); | ||
this.addTags(listItemElm.textContent); | ||
} | ||
// clicked outside the dropdown, so just close it | ||
else onClickOutside(); | ||
} | ||
} | ||
}, | ||
highlightOption: function highlightOption(elm, adjustScroll) { | ||
if (!elm) return; | ||
var className = "tagify__dropdown__item--active"; | ||
// for IE support, which doesn't allow "forEach" on "NodeList" Objects | ||
[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"), function (activeElm) { | ||
return activeElm.classList.remove(className); | ||
}); | ||
// this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className)); | ||
elm.classList.add(className); | ||
if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; | ||
}, | ||
/** | ||
* returns an HTML string of the suggestions' list items | ||
* @return {[type]} [description] | ||
*/ | ||
filterListItems: function filterListItems(value) { | ||
if (!value) return ""; | ||
var list = [], | ||
whitelist = this.settings.whitelist, | ||
suggestionsCount = this.settings.dropdown.maxItems || Infinity, | ||
whitelistItem, | ||
valueIsInWhitelist, | ||
i = 0; | ||
for (; i < whitelist.length; i++) { | ||
whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value: whitelist[i] }, //normalize value as an Object | ||
valueIsInWhitelist = whitelistItem.value.toLowerCase().replace(/\s/g, '').indexOf(value.toLowerCase().replace(/\s/g, '')) == 0; // for fuzzy-search use ">=" | ||
// match for the value within each "whitelist" item | ||
if (valueIsInWhitelist && this.isTagDuplicate(whitelistItem.value) == -1 && suggestionsCount--) list.push(whitelistItem); | ||
if (suggestionsCount == 0) break; | ||
} | ||
return list; | ||
}, | ||
/** | ||
* Creates the dropdown items' HTML | ||
* @param {Array} list [Array of Objects] | ||
* @return {String} | ||
*/ | ||
createListHTML: function createListHTML(list) { | ||
var getItem = this.settings.dropdown.itemTemplate || function (item) { | ||
return '<div class=\'tagify__dropdown__item ' + (item.class ? item.class : "") + '\' ' + getAttributesString(item) + '>' + item.value + '</div>'; | ||
}; | ||
// for a certain Tag element, add attributes. | ||
function getAttributesString(item) { | ||
var i, | ||
keys = Object.keys(item), | ||
s = ""; | ||
for (i = keys.length; i--;) { | ||
var propName = keys[i]; | ||
if (propName != 'class' && !item.hasOwnProperty(propName)) return; | ||
s += " " + propName + (item[propName] ? "=" + item[propName] : ""); | ||
} | ||
return s; | ||
} | ||
return list.map(getItem).join(""); | ||
} | ||
return list.map(getItem).join(""); | ||
} | ||
} | ||
}; | ||
return Tagify; | ||
})); |
@@ -1,1 +0,1 @@ | ||
!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}return t.prototype={isIE:window.document.documentMode,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,autoComplete:!0,mapValueToProp:"",dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},escapeHtml:function(t){var e=document.createTextNode(t),i=document.createElement("p");return i.appendChild(e),i.innerHTML},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';e.originalInput=t,e.scope=this.parseHTML(i),e.input=e.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(e.scope,t),0<=this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)$(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&$(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;t&&this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){this.DOM.input.value&&(t?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest"))},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.toLowerCase()===t.value.toLowerCase()})},markTagByValue:function(t,e){var i;return e||(i=this.isTagDuplicate.call(this,t),e=this.DOM.scope.querySelectorAll("tag")[i]),!!e&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&"value"in t[0],s=[];if(n)return t;if(!n){if(!t.trim())return[];t=t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}})}return!n&&e&&(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),t=s),t},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue.call(n,t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){var n,s=this.getNodeIndex(t);t&&(i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n))))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){var e=this,t=this.value.map(function(t){return t[e.settings.mapValueToProp||"value"]||t.value});this.DOM.originalInput.value=JSON.stringify(t)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),!i||e.length<2?this.dropdown.hide.call(this):(this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this))))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":this.input.autocomplete.set.call(this,e?e.textContent:null)}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}},t}); | ||
!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function t(t,e){if(!t)return console.warn("Tagify: ","invalid input element ",t),this;if(this.settings=this.extend({},this.DEFAULTS,e),this.settings.readonly=t.hasAttribute("readonly"),this.isIE&&(this.settings.autoComplete=!1),t.pattern)try{this.settings.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings&&this.settings.delimiters)try{this.settings.delimiters=new RegExp("["+this.settings.delimiters+"]","g")}catch(t){}this.value=[],this.listeners={},this.DOM={},this.extend(this,new this.EventDispatcher(this)),this.build(t),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this)}return t.prototype={isIE:window.document.documentMode,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:{delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,autoComplete:!0,dropdown:{classname:"",enabled:2,maxItems:10,itemTemplate:""}},customEventsList:["add","remove","invalid"],parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},escapeHtml:function(t){var e=document.createTextNode(t),i=document.createElement("p");return i.appendChild(e),i.innerHTML},build:function(t){var e=this.DOM,i='<tags class="tagify '+t.className+'" '+(this.settings.readonly?"readonly":"")+'>\n <div contenteditable data-placeholder="'+t.placeholder+'" class="tagify__input"></div>\n </tags>';e.originalInput=t,e.scope=this.parseHTML(i),e.input=e.scope.querySelector("[contenteditable]"),t.parentNode.insertBefore(e.scope,t),0<=this.settings.dropdown.enabled&&this.settings.whitelist.length&&this.dropdown.init.call(this),t.autofocus&&e.input.focus()},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope)},extend:function(t,e,i){function n(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function s(t,e){for(var i in e)e.hasOwnProperty(i)&&(n(e[i])?n(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t},EventDispatcher:function(n){var s=document.createTextNode("");this.off=function(t,e){return e&&s.removeEventListener.call(s,t,e),this},this.on=function(t,e){return e&&s.addEventListener.call(s,t,e),this},this.trigger=function(t,e){var i;if(t)if(n.settings.isJQueryPlugin)$(n.DOM.originalInput).triggerHandler(t,[e]);else{try{i=new CustomEvent(t,{detail:e})}catch(t){console.warn(t)}s.dispatchEvent(i)}}},events:{customBinding:function(){var e=this;this.customEventsList.forEach(function(t){e.on(t,e.settings.callbacks[t])})},binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.events.callbacks,i=this.listeners.main=this.listeners.main||{paste:["input",e.onPaste.bind(this)],focus:["input",e.onFocusBlur.bind(this)],blur:["input",e.onFocusBlur.bind(this)],keydown:["input",e.onKeydown.bind(this)],click:["scope",e.onClickScope.bind(this)]},n=t?"addEventListener":"removeEventListener";for(var s in i)this.DOM[i[s][0]][n](s,i[s][1]);t&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",e[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&$(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this)))},callbacks:{onFocusBlur:function(t){var e=t.target.textContent.trim();"focus"==t.type?0===this.settings.dropdown.enabled&&this.dropdown.show.call(this):"blur"==t.type&&e?this.settings.addTagOnBlur&&this.addTags(e,!0).length:(this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this))},onKeydown:function(t){var e,i=t.target.textContent;"Backspace"!=t.key||""!=i&&8203!=i.charCodeAt(0)?"Escape"==t.key||"Esc"==t.key?(this.input.set.call(this),t.target.blur()):"Enter"==t.key?(t.preventDefault(),this.addTags(this.input.value,!0)):"ArrowRight"==t.key&&this.input.autocomplete.set.call(this):(e=(e=this.DOM.scope.querySelectorAll("tag:not(.tagify--hide)"))[e.length-1],this.removeTag(e))},onInput:function(t){var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled;e?this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e).length&&this.input.set.call(this):0<=this.settings.dropdown.enabled&&this.dropdown[i?"show":"hide"].call(this,e)):this.input.set.call(this,"")},onInputIE:function(t){var e=this;setTimeout(function(){e.events.callbacks.onInput.call(e,t)})},onPaste:function(t){},onClickScope:function(t){"TAGS"==t.target.tagName?this.DOM.input.focus():"X"==t.target.tagName&&this.removeTag(t.target.parentNode)}}},loadOriginalValues:function(){var t=this.DOM.originalInput.value;if(t){try{t=JSON.parse(t)}catch(t){}this.addTags(t).forEach(function(t){t&&t.classList.add("tagify--noAnim")})}},input:{value:"",set:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=!(1<arguments.length&&void 0!==arguments[1])||arguments[1];this.input.value=t,e&&(this.DOM.input.innerHTML=t),t||this.dropdown.hide.call(this),t.length<2&&this.input.autocomplete.suggest.call(this,""),this.input.validate.call(this)},setRangeAtEnd:function(){var t,e;document.createRange&&((t=document.createRange()).selectNodeContents(this.DOM.input),t.collapse(!1),(e=window.getSelection()).removeAllRanges(),e.addRange(t))},validate:function(){var t=!this.input.value||this.validateTag.call(this,this.input.value);this.DOM.input.classList.toggle("tagify__input--invalid",!0!==t)},normalize:function(){for(var t=this.DOM.input.cloneNode(!0),e=t.textContent.replace(/\s/g," ");t.firstElementChild;)e+=t.firstElementChild.textContent,t.removeChild(t.firstElementChild);return e.replace(/^\s+/,"")},autocomplete:{suggest:function(t){t&&this.input.value?this.DOM.input.setAttribute("data-suggest",t.substring(this.input.value.length)):this.DOM.input.removeAttribute("data-suggest")},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);i&&(this.input.set.call(this,i),this.input.autocomplete.suggest.call(this,""),this.dropdown.hide.call(this),this.input.setRangeAtEnd.call(this))}}},getNodeIndex:function(t){for(var e=0;t=t.previousSibling;)3==t.nodeType&&/^\s*$/.test(t.data)||e++;return e},isTagDuplicate:function(e){return this.value.findIndex(function(t){return e.trim().toLowerCase()===t.value.toLowerCase()})},getTagIndexByValue:function(i){var n=[];return this.DOM.scope.querySelectorAll("tag").forEach(function(t,e){t.textContent.trim().toLowerCase()==i.toLowerCase()&&n.push(e)}),n},getTagElmByValue:function(t){var e=this.getTagIndexByValue(t)[0];return this.DOM.scope.querySelectorAll("tag")[e]},markTagByValue:function(t,e){return!!(e=e||this.getTagElmByValue(t))&&(e.classList.add("tagify--mark"),setTimeout(function(){e.classList.remove("tagify--mark")},100),e)},isTagBlacklisted:function(e){return e=e.split(" "),this.settings.blacklist.filter(function(t){return-1!=e.indexOf(t)}).length},isTagWhitelisted:function(e){return this.settings.whitelist.some(function(t){if((t.value?t.value:t).toLowerCase()===e.toLowerCase())return!0})},validateTag:function(t){var e=t.trim(),i=this.value.length>=this.settings.maxTags,n=!0;return e?i?n=this.TEXTS.exceed:this.settings.pattern&&!this.settings.pattern.test(e)?n=this.TEXTS.pattern:this.settings.duplicates||-1===this.isTagDuplicate(e)?(this.isTagBlacklisted(e)||this.settings.enforceWhitelist&&!this.isTagWhitelisted(e))&&(n=this.TEXTS.notAllowed):n=this.TEXTS.duplicate:n=this.TEXTS.empty,n},normalizeTags:function(t){var i=this,e=this.settings.whitelist[0]instanceof Object,n=t instanceof Array&&t[0]instanceof Object&&"value"in t[0],s=[];return n?t:"string"==typeof t?t.trim()?t.split(this.settings.delimiters).filter(function(t){return t}).map(function(t){return{value:t.trim()}}):[]:t instanceof Array?t.map(function(t){return{value:t.trim()}}):e?(t.forEach(function(e){var t=i.settings.whitelist.filter(function(t){return t.value.toLowerCase()==e.value.toLowerCase()});t[0]?s.push(t[0]):s.push(e)}),s):void 0},addTags:function(t,e){var n=this,s=[];return this.DOM.input.removeAttribute("style"),(t=this.normalizeTags.call(this,t)).forEach(function(t){var e,i;"function"==typeof n.settings.transformTag&&(t.value=n.settings.transformTag.call(n,t.value)||t.value),!0!==(e=n.validateTag.call(n,t.value))&&(t.class=t.class?t.class+" tagify--notAllowed":"tagify--notAllowed",t.title=e,n.markTagByValue(t.value),n.trigger("invalid",{value:t.value,index:n.value.length,message:e})),i=n.createTagElem(t),s.push(i),function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)}.call(n,i),!0===e?(n.value.push(t),n.update(),n.trigger("add",n.extend({},{index:n.value.length,tag:i},t))):n.settings.keepInvalidTags||setTimeout(function(){n.removeTag(i,!0)},1e3)}),t.length&&e&&this.input.set.call(this),s},createTagElem:function(t){var e,i=this.escapeHtml(t.value),n="<tag title='"+i+"'>\n <x title=''></x><div><span>"+i+"</span></div>\n </tag>";if("function"==typeof this.settings.tagTemplate)try{n=this.settings.tagTemplate(i,t)}catch(t){}return function(t,e){var i,n=Object.keys(e);for(i=n.length;i--;){var s=n[i];if(!e.hasOwnProperty(s))return;t.setAttribute(s,e[s])}}(e=this.parseHTML(n),t),e},removeTag:function(t,e){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:250;if(t){"string"==typeof t&&(t=this.getTagElmByValue(t));var n,s=this.getTagIndexByValue(t.textContent);i&&10<i?(t.style.width=parseFloat(window.getComputedStyle(t).width)+"px",document.body.clientTop,t.classList.add("tagify--hide"),setTimeout(a,400)):a(),e||(n=this.value.splice(s,1)[0],this.update(),this.trigger("remove",this.extend({},{index:s,tag:t},n)))}function a(){t.parentNode.removeChild(t)}},removeAllTags:function(){this.value=[],this.update(),Array.prototype.slice.call(this.DOM.scope.querySelectorAll("tag")).forEach(function(t){return t.parentNode.removeChild(t)})},update:function(){this.DOM.originalInput.value=JSON.stringify(this.value)},dropdown:{init:function(){this.DOM.dropdown=this.dropdown.build.call(this)},build:function(){var t='<div class="'+("tagify__dropdown "+this.settings.dropdown.classname).trim()+'"></div>';return this.parseHTML(t)},show:function(t){var e,i;this.settings.whitelist.length&&(e=t?this.dropdown.filterListItems.call(this,t):this.settings.whitelist.slice(0),i=this.dropdown.createListHTML.call(this,e),this.settings.autoComplete&&this.input.autocomplete.suggest.call(this,e.length?e[0].value:""),this.DOM.dropdown.innerHTML=i,this.dropdown.position.call(this),!this.DOM.dropdown.parentNode!=document.body&&(document.body.appendChild(this.DOM.dropdown),this.events.binding.call(this,!1),this.dropdown.events.binding.call(this)))},hide:function(){this.DOM.dropdown&&this.DOM.dropdown.parentNode==document.body&&(document.body.removeChild(this.DOM.dropdown),window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),this.events.binding.call(this))},position:function(){var t=this.DOM.scope.getBoundingClientRect();this.DOM.dropdown.style.cssText="left: "+(t.left+window.pageXOffset)+"px; top: "+(t.top+t.height-1+window.pageYOffset)+"px; width: "+t.width+"px"},events:{binding:function(){var t=!(0<arguments.length&&void 0!==arguments[0])||arguments[0],e=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:this.dropdown.events.callbacks.onKeyDown.bind(this),onMouseOver:this.dropdown.events.callbacks.onMouseOver.bind(this),onClick:this.dropdown.events.callbacks.onClick.bind(this)},i=t?"addEventListener":"removeEventListener";window[i]("resize",e.position),window[i]("keydown",e.onKeyDown),window[i]("mousedown",e.onClick),this.DOM.dropdown[i]("mouseover",e.onMouseOver)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelectorAll("[class$='--active']")[0],i="";switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault(),e&&(e=e[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),e||(e=this.DOM.dropdown.children["ArrowUp"==t.key||"Up"==t.key?this.DOM.dropdown.children.length-1:0]),this.dropdown.highlightOption.call(this,e,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"Enter":t.preventDefault(),i=e?e.textContent:this.input.value,this.addTags(i,!0),this.dropdown.hide.call(this);break;case"ArrowRight":case"Tab":return t.preventDefault(),this.input.autocomplete.set.call(this,e?e.textContent:null),!1}},onMouseOver:function(t){t.target.className.includes("__item")&&this.dropdown.highlightOption.call(this,t.target)},onClick:function(t){var e,i=this,n=function(){return i.dropdown.hide.call(i)};if(0==t.button){if(t.target==document.documentElement)return n();(e=[t.target,t.target.parentNode].filter(function(t){return t.className.includes("tagify__dropdown__item")})[0])?(this.input.set.call(this),this.addTags(e.textContent)):n()}}}},highlightOption:function(t,e){if(t){var i="tagify__dropdown__item--active";[].forEach.call(this.DOM.dropdown.querySelectorAll("[class$='--active']"),function(t){return t.classList.remove(i)}),t.classList.add(i),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight)}},filterListItems:function(t){if(!t)return"";for(var e,i=[],n=this.settings.whitelist,s=this.settings.dropdown.maxItems||1/0,a=0;a<n.length&&(0==(e=n[a]instanceof Object?n[a]:{value:n[a]}).value.toLowerCase().replace(/\s/g,"").indexOf(t.toLowerCase().replace(/\s/g,""))&&-1==this.isTagDuplicate(e.value)&&s--&&i.push(e),0!=s);a++);return i},createListHTML:function(t){var e=this.settings.dropdown.itemTemplate||function(t){return"<div class='tagify__dropdown__item "+(t.class?t.class:"")+"' "+function(t){var e,i=Object.keys(t),n="";for(e=i.length;e--;){var s=i[e];if("class"!=s&&!t.hasOwnProperty(s))return;n+=" "+s+(t[s]?"="+t[s]:"")}return n}(t)+">"+t.value+"</div>"};return t.map(e).join("")}}},t}); |
{ | ||
"name": "@yaireo/tagify", | ||
"version": "2.1.9", | ||
"version": "2.3.0", | ||
"homepage": "https://github.com/yairEO/tagify", | ||
@@ -17,3 +17,3 @@ "description": "Want to simply convert an input field into a tags element, in a easy customizable way, with good performance and smallest code footprint? You are in the right place my friend.", | ||
}, | ||
"main": "tagify.js", | ||
"main": "./dist/tagify.js", | ||
"repository": { | ||
@@ -27,9 +27,11 @@ "type": "git", | ||
"devDependencies": { | ||
"@babel/core": "^7.1.0", | ||
"@babel/preset-env": "^7.1.0", | ||
"babel-core": "^6.26.3", | ||
"babel-jest": "^23.4.2", | ||
"babel-jest": "^23.6.0", | ||
"babel-preset-env": "^1.7.0", | ||
"beepbeep": "^1.2.2", | ||
"gulp": "^3.9.1", | ||
"gulp-autoprefixer": "4.0.0", | ||
"gulp-babel": "^7.0.1", | ||
"gulp-autoprefixer": "6.0.0", | ||
"gulp-babel": "^8.0.0", | ||
"gulp-cached": "^1.1.1", | ||
@@ -40,19 +42,19 @@ "gulp-clean-css": "^3.10.0", | ||
"gulp-css-globbing": "^0.2.2", | ||
"gulp-eslint": "^3.0.1", | ||
"gulp-eslint": "^5.0.0", | ||
"gulp-insert": "^0.5.0", | ||
"gulp-load-plugins": "^1.5.0", | ||
"gulp-rename": "^1.2.3", | ||
"gulp-replace": "^0.5.4", | ||
"gulp-sass": "^3.2.1", | ||
"gulp-rename": "^1.4.0", | ||
"gulp-replace": "^1.0.0", | ||
"gulp-sass": "^4.0.1", | ||
"gulp-sourcemaps": "^2.6.4", | ||
"gulp-uglify": "^3.0.1", | ||
"gulp-umd": "^0.2.1", | ||
"gulp-umd": "^2.0.0", | ||
"gulp-util": "^3.0.8", | ||
"gulp-watch": "^5.0.1", | ||
"jest": "^23.5.0", | ||
"jsdom": "^11.12.0", | ||
"jest": "^23.6.0", | ||
"jsdom": "^12.0.0", | ||
"path": "^0.12.7", | ||
"puppeteer": "^1.7.0", | ||
"run-sequence": "^1.2.2" | ||
"run-sequence": "^2.2.1", | ||
"puppeteer": "^1.8.0" | ||
} | ||
} |
@@ -21,2 +21,7 @@ [Tagify](https://yaireo.github.io/tagify) - lightweight input "tags" script | ||
npm i @yaireo/tagify --save | ||
// usage: | ||
import Tagify from '@yaireo/tagify' | ||
## [Documentation & Demos](https://yaireo.github.io/tagify) | ||
@@ -47,3 +52,2 @@ | ||
## Building the project | ||
@@ -111,5 +115,6 @@ Simply run `gulp` in your terminal, from the project's path ([Gulp](https://gulpjs.com) should be installed first). | ||
// the original input's value is a String of Array items | ||
// The original input's value is a String representing Array of Objects. | ||
// To parse it, use: `JSON.parse(input.value)` | ||
console.log( input.value ) | ||
// "["tag1", "tag2", ...]" | ||
// "[{value:"tag1"}, {value:"tag2"}, ...]" | ||
``` | ||
@@ -121,3 +126,3 @@ | ||
tagify.value | ||
// [{ "value":"tag1", "class":"red", "id":1}, ...] | ||
// [{"value":"tag1", "class":"red", "id":1}, ...] | ||
``` | ||
@@ -166,4 +171,3 @@ | ||
tagify = new Tagify(input, { | ||
whitelist : allowedTags, | ||
mapValueToProp : "data-id" | ||
whitelist : allowedTags | ||
}); | ||
@@ -190,2 +194,10 @@ | ||
Another way to add tags is: | ||
```javascript | ||
tagify.addTags( | ||
["banana", "orange", "apple"].map( item => ({ value:item }) ) | ||
); | ||
``` | ||
### Suggestions selectbox | ||
@@ -241,8 +253,10 @@ The suggestions selectbox is shown is a whitelist Array of Strings or Objects was passed in the settings when the Tagify instance was created. | ||
Name | Info | ||
--------------- | -------------------------------------------------------------------------- | ||
destroy | Reverts the input element back as it was before Tagify was applied | ||
removeAllTags | Removes all tags and resets the original input tag's value property | ||
addTags | Accepts a String (word, single or multiple with a delimiter) or an Array of Objects (see above) | ||
removeTag | Removes a specific tag (argument is the tag DOM element to be removed. see source code.) | ||
Name | Info | ||
------------------- | -------------------------------------------------------------------------- | ||
destroy | Reverts the input element back as it was before Tagify was applied | ||
removeAllTags | Removes all tags and resets the original input tag's value property | ||
addTags | Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings | ||
removeTag | Removes a specific tag (argument is the tag DOM element to be removed. see source code.) | ||
getTagIndexByValue | | ||
getTagElmByValue | | ||
@@ -273,3 +287,2 @@ | ||
maxTags | Number | Infinity | Maximum number of tags | ||
mapValueToProp | String | "" | For tags with properties, where a certain property should be used as the "saved" value | ||
transformTag | Function | undefined | Takes a tag input as argument and returns a transformed value | ||
@@ -276,0 +289,0 @@ tagTemplate | Function | undefined | Takes a tag's value and data as arguments and returns an HTML string for a tag element |
@@ -39,3 +39,3 @@ function Tagify( input, settings ){ | ||
Tagify.prototype = { | ||
isIE : window.document.documentMode, | ||
isIE : window.document.documentMode, // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility | ||
@@ -62,3 +62,2 @@ TEXTS : { | ||
autoComplete : true, // Flag - tries to autocomplete the input's value while typing | ||
mapValueToProp : "", // String - when tags have multiple properties, and for each tag another property should be used besides the "value" | ||
dropdown : { | ||
@@ -332,4 +331,3 @@ classname : '', | ||
loadOriginalValues(){ | ||
var value = this.DOM.originalInput.value, | ||
values; | ||
var value = this.DOM.originalInput.value; | ||
@@ -339,2 +337,5 @@ // if the original input already had any value (tags) | ||
try{ value = JSON.parse(value) } | ||
catch(err){} | ||
this.addTags(value).forEach(tag => { | ||
@@ -397,3 +398,3 @@ tag && tag.classList.add('tagify--noAnim'); | ||
/** | ||
* suggest the rest of the input's value | ||
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)") | ||
* @param {String} s [description] | ||
@@ -403,5 +404,6 @@ */ | ||
suggest(s){ | ||
if( !this.DOM.input.value ) return; // do not suggest anything for empty input | ||
if( s ) this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length)); | ||
else this.DOM.input.removeAttribute("data-suggest"); | ||
if( !s || !this.input.value ) | ||
this.DOM.input.removeAttribute("data-suggest"); | ||
else | ||
this.DOM.input.setAttribute("data-suggest", s.substring(this.input.value.length)); | ||
}, | ||
@@ -441,6 +443,20 @@ set(s){ | ||
isTagDuplicate(s){ | ||
return this.value.findIndex(item => s.toLowerCase() === item.value.toLowerCase()); | ||
return this.value.findIndex(item => s.trim().toLowerCase() === item.value.toLowerCase()); | ||
// return this.value.some(item => s.toLowerCase() === item.value.toLowerCase()); | ||
}, | ||
getTagIndexByValue( value ){ | ||
var result = []; | ||
this.DOM.scope.querySelectorAll('tag').forEach((tagElm, i) => { | ||
if( tagElm.textContent.trim().toLowerCase() == value.toLowerCase() ) | ||
result.push(i) | ||
}) | ||
return result; | ||
}, | ||
getTagElmByValue( value ){ | ||
var tagIdx = this.getTagIndexByValue(value)[0]; | ||
return this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
}, | ||
/** | ||
@@ -453,8 +469,5 @@ * Mark a tag element by its value | ||
markTagByValue(value, tagElm){ | ||
var tagsElms, tagsElmsLen, tagIdx | ||
var tagsElms, tagsElmsLen | ||
if( !tagElm ){ | ||
tagIdx = this.isTagDuplicate.call(this, value); | ||
tagElm = this.DOM.scope.querySelectorAll('tag')[tagIdx]; | ||
} | ||
tagElm = tagElm || this.getTagElmByValue(value); | ||
@@ -528,5 +541,5 @@ // check AGAIN if "tagElm" is defined | ||
*/ | ||
normalizeTags(tagsItems){ | ||
normalizeTags( tagsItems ){ | ||
var whitelistWithProps = this.settings.whitelist[0] instanceof Object, | ||
isComplex = tagsItems instanceof Array && "value" in tagsItems[0], // checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
isComplex = tagsItems instanceof Array && tagsItems[0] instanceof Object && "value" in tagsItems[0], // checks if the value is a "complex" which means an Array of Objects, each object is a tag | ||
temp = []; | ||
@@ -540,11 +553,14 @@ | ||
// if the value is a "simple" String, ex: "aaa, bbb, ccc" | ||
if( !isComplex ){ | ||
if( typeof tagsItems == 'string' ){ | ||
if( !tagsItems.trim() ) return []; | ||
// go over each tag and add it (if there were multiple ones) | ||
tagsItems = tagsItems.split(this.settings.delimiters).filter(n => n).map(v => ({ value:v.trim() })); | ||
return tagsItems.split(this.settings.delimiters).filter(n => n).map(v => ({ value:v.trim() })); | ||
} | ||
if( tagsItems instanceof Array ) | ||
return tagsItems.map(v => ({ value:v.trim() })) | ||
// search if the tag exists in the whitelist as an Object (has props), to be able to use its properties | ||
if( !isComplex && whitelistWithProps ){ | ||
if( whitelistWithProps ){ | ||
tagsItems.forEach(tag => { | ||
@@ -558,7 +574,4 @@ var matchObj = this.settings.whitelist.filter( WL_item => WL_item.value.toLowerCase() == tag.value.toLowerCase() ) | ||
tagsItems = temp; | ||
return temp; | ||
} | ||
return tagsItems; | ||
}, | ||
@@ -568,3 +581,3 @@ | ||
* add a "tag" element to the "tags" component | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects] | ||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] | ||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] | ||
@@ -592,3 +605,3 @@ * @return {Array} Array of DOM elements (tags) | ||
tagData.title = tagValidation; | ||
this.markTagByValue.call(this, tagData.value); | ||
this.markTagByValue(tagData.value); | ||
this.trigger("invalid", {value:tagData.value, index:this.value.length, message:tagValidation}); | ||
@@ -675,5 +688,5 @@ } | ||
* Removes a tag | ||
* @param {Object} tagElm [DOM element] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
* @param {Object|String} tagElm [DOM element or a String value] | ||
* @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] | ||
* @param {Number} tranDuration [Transition duration in MS] | ||
*/ | ||
@@ -683,7 +696,8 @@ removeTag( tagElm, silent, tranDuration = 250 ){ | ||
if( typeof tagElm == 'string' ) | ||
tagElm = this.getTagElmByValue(tagElm) | ||
var tagData, | ||
tagIdx = this.getNodeIndex(tagElm); | ||
tagIdx = this.getTagIndexByValue(tagElm.textContent); //this.getNodeIndex(tagElm); (getNodeIndex is unreliable) | ||
if( !tagElm) return; | ||
if( tranDuration && tranDuration > 10 ) animation() | ||
@@ -723,4 +737,3 @@ else removeNode(); | ||
update(){ | ||
var tagsAsString = this.value.map(v => v[this.settings.mapValueToProp || "value"] || v.value); | ||
this.DOM.originalInput.value = JSON.stringify(tagsAsString); | ||
this.DOM.originalInput.value = JSON.stringify(this.value); | ||
}, | ||
@@ -748,2 +761,4 @@ | ||
// if no value was supplied, show all the "whitelist" items in the dropdown | ||
// @type [Array] listItems | ||
listItems = value ? | ||
@@ -761,6 +776,6 @@ this.dropdown.filterListItems.call(this, value) : | ||
if( !listHTML || listItems.length < 2 ){ | ||
this.dropdown.hide.call(this); | ||
return; | ||
} | ||
// if( !listHTML || listItems.length < 2 ){ | ||
// this.dropdown.hide.call(this); | ||
// return; | ||
// } | ||
@@ -859,4 +874,6 @@ this.DOM.dropdown.innerHTML = listHTML | ||
case 'ArrowRight' : | ||
case 'Tab' : | ||
e.preventDefault(); | ||
this.input.autocomplete.set.call(this, selectedElm ? selectedElm.textContent : null); | ||
break; | ||
return false; | ||
} | ||
@@ -863,0 +880,0 @@ }, |
2634
285
318384
30