├── .gitignore
├── package.json
├── webpack.mix.js
├── index.html
├── README.md
├── src
├── inputTags.less
├── inputTags.css
└── index.js
└── dist
├── inputTags.min.css
└── inputTags.jquery.min.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules/*
3 | /.vs
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "inputTags.js",
3 | "version": "1.0.0",
4 | "description": "inputTags.js is a simple jQuery plugin allowing to add, edit or remove tags in a tags list.",
5 | "scripts": {
6 | "build": "cross-env NODE_ENV=production npx mix"
7 | },
8 | "devDependencies": {
9 | "cross-env": "^7.0.3",
10 | "laravel-mix": "^6.0.43",
11 | "less": "^4.1.2",
12 | "less-loader": "^10.2.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | // webpack.mix.js
2 |
3 | let mix = require('laravel-mix')
4 | let { rmSync, renameSync } = require('fs')
5 |
6 | mix
7 | .disableNotifications()
8 | .disableSuccessNotifications()
9 | .before(() => {
10 | try {
11 | rmSync('src/inputTags.css')
12 | } catch {}
13 |
14 | try {
15 | rmSync('dist/', { recursive: true, force: true })
16 | } catch {}
17 | })
18 | .js('src/index', 'dist/inputTags.jquery')
19 | .less('src/inputTags.less', 'src/inputTags.css')
20 | .minify([
21 | 'dist/inputTags.jquery.js',
22 | 'src/inputTags.css'
23 | ])
24 | .options({ manifest: false })
25 | .after(() => {
26 | setTimeout(() => {
27 | rmSync('dist/inputTags.jquery.js')
28 | renameSync('src/inputTags.min.css', 'dist/inputTags.min.css')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ").addClass(o.LIST_CLASS),o.$input=e("
").attr({type:"text",class:o.FIELD_CLASS}),o.$html.insertAfter(o.$element).prepend(o.$input),o.$list=o.$element.next("."+o.LIST_CLASS),o.$list.on("click",(function(t){if(e(t.target).hasClass("inputTags-field"))return!1;o.$input.focus()}))},o.fill=function(){if(o._getDefaultValues(),0===o.options.tags)return!1;o._concatenate(),o._updateValue(),o._fill()},o._fill=function(){o.tags.forEach((function(t,e){var n=o._validate(t,!1);(!0===n||"max"===n&&e+1<=o.options.max)&&o._buildItem(t)}))},o._clean=function(){e("."+o.ITEM_CLASS,o.$list).remove()},o.save=function(){o.$input.on("keyup",(function(t){t.preventDefault();var n=t.key,i=o.$input.val().trim();if(e.inArray(n,o.keys)<0)return o._autocomplete._init(!0),o._autocomplete._show(),!1;if(o.KEY_ESCAPE===n)return o._cancel(),o._autocomplete._hide(),!1;if(i=o.KEY_COMMA===n?i.slice(0,-1):i,!o._validate(i,!0))return!1;if(o.options.only&&o._exists(i))return o._errors("exists"),!1;if(o.$input.hasClass("is-edit")){var s=o.$input.attr("data-old-value");if(s===i)return o._cancel(),!0;o._update(s,i),o._clean(),o._fill()}else{if(o._autocomplete._isSet()&&o._autocomplete._get("only")&&e.inArray(i,o._autocomplete._get("values"))<0)return o._autocomplete._hide(),o._errors("autocomplete_only"),!1;if(o._exists(i)){o.$input.removeClass("is-autocomplete"),o._errors("exists");var a=e('[data-tag="'+i+'"]',o.$list);return a.addClass("is-exists"),setTimeout((function(){a.removeClass("is-exists")}),300),!1}o._buildItem(i),o._insert(i)}return o._cancel(),o._updateValue(),o.destroy(),o._autocomplete._build(),o._setInstance(o),o.$input.focus(),!1}))},o.edit=function(){o.$list.on("click","."+o.ITEM_CLASS,(function(t){if(e(t.target).hasClass("close-item")||!1===o.options.editable||o._autocomplete._isSet()&&o._autocomplete._get("only"))return o._cancel(),!0;var n=e(this).addClass("is-edit"),i=e(".value",n).text();o.$input.width(n.outerWidth()).insertAfter(n).addClass("is-edit").attr("data-old-value",i).val(i).focus(),o._bindEvent("selected"),o.$input.on("blur",(function(){o._cancel(),o._bindEvent("unselected")}))}))},o.destroy=function(){e("."+o.ITEM_CLASS,o.$list).off("click").on("click",".close-item",(function(){var t=e(this).parent("."+o.ITEM_CLASS),n=e(".value",t).text();t.addClass("is-closed"),setTimeout((function(){o._pop(n),o._updateValue(),t.remove(),o._autocomplete._build(),o.$input.focus(),o._setInstance(o)}),200)}))},o._buildItem=function(t){var n=e(o.ITEM_CONTENT.replace("%s",t));e("
").addClass(o.ITEM_CLASS+" is-closed").attr("data-tag",t).html(n).insertBefore(o.$input).delay(100).queue((function(){e(this).removeClass("is-closed")}))},o._getIndex=function(t){return o.tags.indexOf(t)},o._concatenate=function(){o.options.max>0&&o.options.tags.length>o.options.max&&o.options.tags.splice(-Math.abs(o.options.tags.length-o.options.max)),o.tags=o.tags.concat(o.options.tags)},o._getDefaultValues=function(){o.$element.val().length>0?o.tags=o.tags.concat(o.$element.val().split(o.KEY_COMMA)):o.$element.prop("value","")},o._insert=function(t){o.tags.push(t),o._bindEvent(["change","create"])},o._update=function(t,e){var n=o._getIndex(t);o.tags[n]=e,o._bindEvent(["change","update"])},o._pop=function(t){var e=o._getIndex(t);if(e<0)return!1;o.tags.splice(e,1),o._bindEvent(["change","destroy"])},o._cancel=function(){e("."+o.ITEM_CLASS).removeClass("is-edit"),o.$input.removeClass("is-edit is-autocomplete").removeAttr("data-old-value style").val("").appendTo(o.$list)},o._autocomplete={_isSet:function(){return o.options.autocomplete.values.length>0},_init:function(t){if(!o._autocomplete._isSet())return!1;o._autocomplete._build(t)},_build:function(t){var n=o.$input.val().trim().toLowerCase();o._autocomplete._exists()&&o.$autocomplete.remove(),o.$autocomplete=e("").addClass(o.AUTOCOMPLETE_LIST_CLASS),o._autocomplete._get("values").forEach((function(i){var s=o.AUTOCOMPLETE_ITEM_CONTENT.replace("%s",i),a=e(s);e.inArray(i,o.tags)>=0&&a.addClass("is-disabled"),t&&n.length>0&&!i.toLowerCase().startsWith(n)&&a.css({display:"none"}),a.appendTo(o.$autocomplete)})),o._autocomplete._bindClick(),e(document).not(o.$autocomplete).on("click",(function(){o._autocomplete._hide()}))},_bindClick:function(){e(o.$autocomplete).off("click").on("click","."+o.AUTOCOMPLETE_ITEM_CLASS,(function(t){if(e(t.target).hasClass("is-disabled"))return!1;o.$input.addClass("is-autocomplete").val(e(this).text()),o._autocomplete._hide(),o._bindEvent("autocompleteTagSelect"),(t=e.Event("keyup")).key=o.KEY_ENTER,o.$input.trigger(t)}))},_show:function(){if(!o._autocomplete._isSet())return!1;o.$autocomplete.css({left:o.$input[0].offsetLeft,minWidth:o.$input.width()}).insertAfter(o.$input),setTimeout((function(){o._autocomplete._bindClick(),o.$autocomplete.addClass("is-active")}),100)},_hide:function(){o.$autocomplete.removeClass("is-active")},_get:function(t){return o.options.autocomplete[t]},_exists:function(){return void 0!==o.$autocomplete}},o._updateValue=function(){o.$element.prop("value",o.tags.join(o.KEY_COMMA))},o._focus=function(){o.$input.on("focus",(function(){o._bindEvent("focus"),!o._autocomplete._isSet()||o.$input.hasClass("is-autocomplete")||o.$input.hasClass("is-edit")||(o._autocomplete._build(),o._autocomplete._show())}))},o._toObject=function(t){return t.reduce((function(t,e,n){return t[n]=e,t}),{})},o._validate=function(t,e){var n="";switch(!0){case!t:case void 0===t:case 0===t.length:o._cancel(),n="empty";break;case t.length>0&&t.lengtho.options.maxLength:n="maxLength";break;case o.options.max>0&&o.tags.length>=o.options.max:o.$input.hasClass("is-edit")||(n="max");break;case o.options.email:/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(t)||(n="email")}return!(n.length>0)||(e?o._errors(n):n)},o._exists=function(t){return e.inArray(t,o.tags)>=0},o._errors=function(t){return 0===t.length||(o._autocomplete._exists()&&o.$autocomplete.remove(),o._displayErrors(o.options.errors[t].replace("%s",o.options[t]),t)),!1},o._displayErrors=function(t,n){var i=e(o.ERROR_CONTENT.replace("%s",t)).attr("data-error",n),s=o.options.errors.timeout;return!e("."+o.ERROR_CLASS+'[data-error="'+n+'"]').length&&(i.hide().insertAfter(o.$list).slideDown(),!(!s||s<=0)&&(e("."+o.ERROR_CLASS).on("click",(function(){o._collapseErrors(e(this))})),void setTimeout((function(){o._collapseErrors()}),s)))},o._collapseErrors=function(t){var n=t||e("."+o.ERROR_CLASS);n.slideUp(300,(function(){n.remove()}))},o._getInstance=function(){return window.inputTags.instances[o.UNIQID]},o._setInstance=function(){window.inputTags.instances[o.UNIQID]=o},o._isSet=function(t){return!(void 0===o.options[t]||!1===o.options[t]||o.options[t].length)},o._callMethod=function(t,e){if(void 0===e.options[t]||"function"!=typeof e.options[t])return!1;e.options[t].apply(this,Array.prototype.slice.call(arguments,1))},o._initEvent=function(e,n){if(!e)return!1;switch(t(e)){case"string":n(e,o);break;case"object":e.forEach((function(t,e){n(t,o)}))}return!0},o._bindEvent=function(t){return o._initEvent(t,(function(t,e){o._callMethod(t,e)}))},o._unbindEvent=function(t){return o._initEvent(t,(function(t){o.options[t]=!1}))},o.init(),o._bindEvent("init"),o._setInstance(o)})),{on:function(t,e){window.inputTags.methods.event(t,e)}};if(window.inputTags.methods[n]){var o=e(this).attr("data-uniqid"),i=window.inputTags.instances[o];return void 0===i?e.error("[undefined instance] No inputTags instance found."):window.inputTags.methods[n].apply(this,Array.prototype.slice.call(arguments,1))}e.error("[undefined method] The method ["+n+"] does not exists.")},e.fn.inputTags.defaults={tags:[],keys:[],minLength:2,maxLength:30,max:6,email:!1,only:!0,init:!1,create:!1,update:!1,destroy:!1,focus:!1,selected:!1,unselected:!1,change:!1,autocompleteTagSelect:!1,editable:!0,autocomplete:{values:[],only:!1},errors:{empty:"Attention, vous ne pouvez pas ajouter un tag vide.",minLength:"Attention, votre tag doit avoir au minimum %s caractères.",maxLength:"Attention, votre tag ne doit pas dépasser %s caractères.",max:"Attention, le nombre de tags ne doit pas dépasser %s.",email:"Attention, l'adresse email que vous avez entré n'est pas valide",exists:"Attention, ce tag existe déjà !",autocomplete_only:"Attention, vous devez sélectionner une valeur dans la liste.",timeout:8e3}}},829:()=>{}},n={};function o(t){var i=n[t];if(void 0!==i)return i.exports;var s=n[t]={exports:{}};return e[t](s,s.exports,o),s.exports}o.m=e,t=[],o.O=(e,n,i,s)=>{if(!n){var a=1/0;for(c=0;c=s)&&Object.keys(o.O).every((t=>o.O[t](n[u])))?n.splice(u--,1):(r=!1,s0&&t[c-1][2]>s;c--)t[c]=t[c-1];t[c]=[n,i,s]},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t={249:0,6:0};o.O.j=e=>0===t[e];var e=(e,n)=>{var i,s,[a,r,u]=n,l=0;if(a.some((e=>0!==t[e]))){for(i in r)o.o(r,i)&&(o.m[i]=r[i]);if(u)var c=u(o)}for(e&&e(n);lo(419)));var i=o.O(void 0,[6],(()=>o(829)));i=o.O(i)})();
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 |
3 | $.fn.inputTags = function (options) {
4 | if (!('inputTags' in window)) {
5 | window.inputTags = {
6 | instances: []
7 | };
8 | }
9 |
10 | window.inputTags.methods = {
11 | tags: function (element, callback) {
12 | if (element) {
13 | switch (typeof element) {
14 | case 'string':
15 | switch (element) {
16 | case '_toString':
17 | var str = _instance.tags.toString();
18 |
19 | if (callback) {
20 | return callback(str);
21 | }
22 | return str;
23 | case '_toObject':
24 | var obj = _instance._toObject(_instance.tags);
25 |
26 | if (callback) {
27 | return callback(obj);
28 | }
29 | return obj;
30 | case '_toJSON':
31 | var obj = _instance._toObject(_instance.tags);
32 | var json = JSON.stringify(obj);
33 |
34 | if (callback) {
35 | return callback(json);
36 | }
37 | return json;
38 | case '_toArray':
39 | if (callback) {
40 | return callback(_instance.tags);
41 | }
42 | return _instance.tags;
43 | }
44 |
45 | var partials = element.split(',');
46 |
47 | if (partials.length > 1) {
48 | var current = _instance.tags;
49 | _instance.tags = current.concat(partials);
50 | } else {
51 | _instance.tags.push(partials[0]);
52 | }
53 | break;
54 | case 'object':
55 | var current = _instance.tags;
56 |
57 | if ('[object Object]' === Object.prototype.toString.call(element)) {
58 | element = Object.keys(element).map(function (k) {
59 | return element[k];
60 | });
61 | }
62 |
63 | _instance.tags = current.concat(element);
64 | break;
65 | case 'function':
66 | return element(_instance.tags);
67 | }
68 |
69 | _instance._clean();
70 | _instance._fill();
71 | _instance._updateValue();
72 |
73 | _instance.destroy();
74 |
75 | _instance._setInstance(_instance);
76 |
77 | if (callback) {
78 | return callback(_instance.tags);
79 | }
80 | }
81 |
82 | return _instance.tags;
83 | },
84 | event: function (method, callback) {
85 | _instance.options[method] = callback;
86 | _instance._setInstance(_instance);
87 | },
88 | options: function (key, value) {
89 | if (!key && !value) {
90 | return _instance.options;
91 | }
92 |
93 | if (value) {
94 | _instance.options[key] = value;
95 | _instance._setInstance(_instance);
96 | } else {
97 | return _instance.options[key];
98 | }
99 | },
100 | destroy: function () {
101 | var id = $(this).attr('data-uniqid');
102 | delete window.inputTags.instances[id];
103 | }
104 | };
105 |
106 | if ('object' === typeof options || !options) {
107 | var options = $.extend(true, {}, $.fn.inputTags.defaults, options);
108 |
109 | this.each(function () {
110 | var self = $(this);
111 |
112 | /* Constants */
113 | self.KEY_ENTER = 'Enter';
114 | self.KEY_COMMA = ',';
115 | self.KEY_ESCAPE = 'Escape';
116 | self.UNIQID = Math.round(Date.now() / (Math.random() * (548 - 54) - 54));
117 | self.DEFAULT_CLASS = 'inputTags';
118 | self.ELEMENT_CLASS = self.DEFAULT_CLASS + '-' + self.UNIQID;
119 | self.LIST_CLASS = self.DEFAULT_CLASS + '-list';
120 | self.ITEM_CLASS = self.DEFAULT_CLASS + '-item';
121 | self.ITEM_CONTENT = '%s×';
122 | self.FIELD_CLASS = self.DEFAULT_CLASS + '-field';
123 | self.ERROR_CLASS = self.DEFAULT_CLASS + '-error';
124 | self.ERROR_CONTENT = '%s
';
125 |
126 | self.AUTOCOMPLETE_LIST_CLASS = self.DEFAULT_CLASS + '-autocomplete-list';
127 | self.AUTOCOMPLETE_ITEM_CLASS = self.DEFAULT_CLASS + '-autocomplete-item';
128 | self.AUTOCOMPLETE_ITEM_CONTENT = '- %s
';
129 |
130 | /* Variables */
131 | self.options = options;
132 | self.keys = [self.KEY_ENTER, self.KEY_COMMA, self.KEY_ESCAPE];
133 | self.tags = [];
134 |
135 | if (self.options.keys.length > 0) {
136 | self.keys = self.keys.concat(self.options.keys);
137 | }
138 |
139 | self.init = function () {
140 | self.addClass(self.ELEMENT_CLASS).attr('data-uniqid', self.UNIQID);
141 |
142 | self.$element = $('.' + self.ELEMENT_CLASS);
143 | self.$element.hide();
144 |
145 | /* initialization */
146 | self.build();
147 | self.fill();
148 | self.save();
149 | self.edit();
150 | self.destroy();
151 | self._autocomplete._init();
152 | self._focus();
153 | };
154 |
155 | /**
156 | * Build plugin's HTML skeleton
157 | *
158 | * @returns {void}
159 | */
160 | self.build = function () {
161 | self.$html = $('').addClass(self.LIST_CLASS);
162 | self.$input = $('
').attr({
163 | 'type': 'text',
164 | 'class': self.FIELD_CLASS
165 | });
166 |
167 | self.$html.insertAfter(self.$element).prepend(self.$input);
168 |
169 | self.$list = self.$element.next('.' + self.LIST_CLASS);
170 |
171 | self.$list.on('click', function (e) {
172 | if ($(e.target).hasClass('inputTags-field')) {
173 | return false;
174 | }
175 | self.$input.focus();
176 | });
177 | };
178 |
179 | /**
180 | * Init tags list if present in options, otherwise returns false
181 | *
182 | * @returns {void | boolean}
183 | */
184 | self.fill = function () {
185 | self._getDefaultValues();
186 |
187 | if (0 === self.options.tags) {
188 | return false;
189 | }
190 |
191 | self._concatenate();
192 | self._updateValue();
193 |
194 | self._fill();
195 | };
196 |
197 | /**
198 | * Fills tag list
199 | *
200 | * @returns {void}
201 | */
202 | self._fill = function () {
203 | self.tags.forEach(function (value, i) {
204 | var validate = self._validate(value, false);
205 |
206 | if (true === validate || ('max' === validate && i + 1 <= self.options.max)) {
207 | self._buildItem(value);
208 | }
209 | });
210 | };
211 |
212 | /**
213 | * Clear HTML tags list
214 | *
215 | * @returns {void}
216 | */
217 | self._clean = function () {
218 | $('.' + self.ITEM_CLASS, self.$list).remove();
219 | };
220 |
221 | /**
222 | * Add or edit tag depends on key pressed
223 | *
224 | * @returns {void}
225 | */
226 | self.save = function () {
227 | self.$input.on('keyup', function (e) {
228 | e.preventDefault();
229 |
230 | var key = e.key;
231 | var value = self.$input.val().trim();
232 |
233 | if ($.inArray(key, self.keys) < 0) {
234 | self._autocomplete._init(true);
235 | self._autocomplete._show();
236 |
237 | return false;
238 | }
239 |
240 | if (self.KEY_ESCAPE === key) {
241 | self._cancel();
242 | self._autocomplete._hide();
243 |
244 | return false;
245 | }
246 |
247 | value = self.KEY_COMMA === key ? value.slice(0, -1) : value;
248 |
249 | if (!self._validate(value, true)) {
250 | return false;
251 | }
252 |
253 | if (self.options.only && self._exists(value)) {
254 | self._errors('exists');
255 |
256 | return false;
257 | }
258 |
259 | if (self.$input.hasClass('is-edit')) {
260 | var old_value = self.$input.attr('data-old-value');
261 |
262 | if (old_value === value) {
263 | self._cancel();
264 | return true;
265 | }
266 |
267 | self._update(old_value, value);
268 | self._clean();
269 | self._fill();
270 | } else {
271 | if (self._autocomplete._isSet() && self._autocomplete._get('only')) {
272 | if ($.inArray(value, self._autocomplete._get('values')) < 0) {
273 | self._autocomplete._hide();
274 | self._errors('autocomplete_only');
275 | return false;
276 | }
277 | }
278 |
279 | if (self._exists(value)) {
280 | self.$input.removeClass('is-autocomplete');
281 | self._errors('exists');
282 |
283 | var $tag = $('[data-tag="' + value + '"]', self.$list);
284 |
285 | $tag.addClass('is-exists');
286 |
287 | setTimeout(function () {
288 | $tag.removeClass('is-exists');
289 | }, 300);
290 | return false;
291 | }
292 |
293 | self._buildItem(value);
294 | self._insert(value);
295 | }
296 |
297 | self._cancel();
298 | self._updateValue();
299 | self.destroy();
300 | self._autocomplete._build();
301 |
302 | self._setInstance(self);
303 |
304 | self.$input.focus();
305 |
306 | return false;
307 | });
308 | };
309 |
310 | /**
311 | * Init edit input when a tag is focused
312 | *
313 | * @returns {void}
314 | */
315 | self.edit = function () {
316 | self.$list.on('click', '.' + self.ITEM_CLASS, function (e) {
317 | if ($(e.target).hasClass('close-item') || false === self.options.editable || (self._autocomplete._isSet() && self._autocomplete._get('only'))) {
318 | self._cancel();
319 | return true;
320 | }
321 |
322 | var $item = $(this).addClass('is-edit');
323 | var value = $('.value', $item).text();
324 |
325 | self.$input.width($item.outerWidth()).insertAfter($item).addClass('is-edit').attr('data-old-value', value).val(value).focus();
326 |
327 | self._bindEvent('selected');
328 |
329 | self.$input.on('blur', function () {
330 | self._cancel();
331 | self._bindEvent('unselected');
332 | });
333 | });
334 | };
335 |
336 | /**
337 | * Delete tag
338 | *
339 | * @returns {void}
340 | */
341 | self.destroy = function () {
342 | $('.' + self.ITEM_CLASS, self.$list).off('click').on('click', '.close-item', function () {
343 |
344 | var $item = $(this).parent('.' + self.ITEM_CLASS);
345 | var value = $('.value', $item).text();
346 |
347 | $item.addClass('is-closed');
348 |
349 | setTimeout(function () {
350 | self._pop(value);
351 | self._updateValue();
352 | $item.remove();
353 |
354 | self._autocomplete._build();
355 |
356 | self.$input.focus();
357 |
358 | self._setInstance(self);
359 | }, 200);
360 | });
361 | };
362 |
363 | /**
364 | * Build and inject tag into HTML list
365 | *
366 | * @returns {void}
367 | */
368 | self._buildItem = function (value) {
369 | var $content = $(self.ITEM_CONTENT.replace('%s', value));
370 | var $item = $('
').addClass(self.ITEM_CLASS + ' is-closed').attr('data-tag', value).html($content);
371 |
372 | $item.insertBefore(self.$input).delay(100).queue(function () {
373 | $(this).removeClass('is-closed');
374 | });
375 | };
376 |
377 | /**
378 | * Returns tag index
379 | *
380 | * @returns {number}
381 | */
382 | self._getIndex = function (value) {
383 | return self.tags.indexOf(value);
384 | };
385 |
386 | /**
387 | * Remove extra tags only if > max option and concat user tags
388 | *
389 | * @returns {void}
390 | */
391 | self._concatenate = function () {
392 | if (self.options.max > 0) {
393 | if (self.options.tags.length > self.options.max) {
394 | self.options.tags.splice(-Math.abs(self.options.tags.length - self.options.max));
395 | }
396 | }
397 |
398 | self.tags = self.tags.concat(self.options.tags);
399 | };
400 |
401 |
402 | /**
403 | * Get default values
404 | *
405 | * @returns {void}
406 | */
407 | self._getDefaultValues = function () {
408 | if (self.$element.val().length > 0) {
409 | self.tags = self.tags.concat(self.$element.val().split(self.KEY_COMMA));
410 | } else {
411 | self.$element.prop('value', '');
412 | }
413 | };
414 |
415 | /**
416 | * Insert item
417 | *
418 | * @param {string} item
419 | * @returns {void}
420 | */
421 | self._insert = function (item) {
422 | self.tags.push(item);
423 |
424 | self._bindEvent(['change', 'create']);
425 | };
426 |
427 | /**
428 | * Swap tag value
429 | *
430 | * @param {string} old_value
431 | * @param {string} new_value
432 | * @returns {void}
433 | */
434 | self._update = function (old_value, new_value) {
435 | var index = self._getIndex(old_value);
436 | self.tags[index] = new_value;
437 |
438 | self._bindEvent(['change', 'update']);
439 | };
440 |
441 | /**
442 | * Delete item based on value parameter
443 | *
444 | * @param {string} value
445 | * @returns {void}
446 | */
447 | self._pop = function (value) {
448 | var index = self._getIndex(value);
449 |
450 | if (index < 0) {
451 | return false;
452 | }
453 |
454 | self.tags.splice(index, 1);
455 |
456 | self._bindEvent(['change', 'destroy']);
457 | };
458 |
459 | /**
460 | * Reset input field
461 | *
462 | * @returns {void}
463 | */
464 | self._cancel = function () {
465 | $('.' + self.ITEM_CLASS).removeClass('is-edit');
466 |
467 | self.$input
468 | .removeClass('is-edit is-autocomplete')
469 | .removeAttr('data-old-value style')
470 | .val('')
471 | .appendTo(self.$list);
472 | };
473 |
474 | /**
475 | * Autocomplete object
476 | *
477 | * @returns {object}
478 | */
479 | self._autocomplete = {
480 |
481 | /**
482 | * Is autocomplete list have values
483 | *
484 | * @returns {boolean}
485 | */
486 | _isSet: function () {
487 | return self.options.autocomplete.values.length > 0;
488 | },
489 |
490 | /**
491 | * Init autocomplete
492 | *
493 | * @param {boolean | undefined} filter
494 | * @returns {boolean | void}
495 | */
496 | _init: function (filter) {
497 | if (!self._autocomplete._isSet()) {
498 | return false;
499 | }
500 |
501 | self._autocomplete._build(filter);
502 | },
503 |
504 | /**
505 | * Build autocomplete HTML list
506 | *
507 | * @param {boolean | undefined} filter
508 | * @returns {void}
509 | */
510 | _build: function (filter) {
511 | var value = self.$input.val().trim().toLowerCase();
512 |
513 | if (self._autocomplete._exists()) {
514 | self.$autocomplete.remove();
515 | }
516 |
517 | self.$autocomplete = $('').addClass(self.AUTOCOMPLETE_LIST_CLASS);
518 |
519 | self._autocomplete._get('values').forEach(function (v) {
520 | var li = self.AUTOCOMPLETE_ITEM_CONTENT.replace('%s', v);
521 | var $item = $(li)
522 |
523 | if ($.inArray(v, self.tags) >= 0) {
524 | $item.addClass('is-disabled')
525 | }
526 |
527 | if (filter && value.length > 0 && !v.toLowerCase().startsWith(value)) {
528 | $item.css({ display: 'none' })
529 | }
530 |
531 | $item.appendTo(self.$autocomplete);
532 | });
533 |
534 | self._autocomplete._bindClick();
535 |
536 | $(document)
537 | .not(self.$autocomplete)
538 | .on('click', function () {
539 | self._autocomplete._hide();
540 | });
541 | },
542 |
543 | /**
544 | * Bind click event on list item
545 | *
546 | * @returns {void}
547 | */
548 | _bindClick: function () {
549 | $(self.$autocomplete).off('click').on('click', '.' + self.AUTOCOMPLETE_ITEM_CLASS, function (e) {
550 | if ($(e.target).hasClass('is-disabled')) {
551 | return false;
552 | }
553 |
554 | self.$input.addClass('is-autocomplete').val($(this).text());
555 | self._autocomplete._hide();
556 | self._bindEvent('autocompleteTagSelect');
557 |
558 | var e = $.Event("keyup");
559 | e.key = self.KEY_ENTER;
560 | self.$input.trigger(e);
561 | });
562 | },
563 |
564 | /**
565 | * Show autocomplete list
566 | *
567 | * @returns {boolean | void}
568 | */
569 | _show: function () {
570 | if (!self._autocomplete._isSet()) {
571 | return false;
572 | }
573 |
574 | self.$autocomplete
575 | .css({
576 | 'left': self.$input[0].offsetLeft,
577 | 'minWidth': self.$input.width()
578 | })
579 | .insertAfter(self.$input);
580 |
581 | setTimeout(function () {
582 | self._autocomplete._bindClick();
583 | self.$autocomplete.addClass('is-active');
584 | }, 100);
585 | },
586 |
587 | /**
588 | * Hide autocomplete list
589 | *
590 | * @returns {void}
591 | */
592 | _hide: function () {
593 | self.$autocomplete.removeClass('is-active');
594 | },
595 |
596 | /**
597 | * Get autocomplete item based on key parameter
598 | *
599 | * @param {string} key
600 | * @returns {boolean | void}
601 | */
602 | _get: function (key) {
603 | return self.options.autocomplete[key];
604 | },
605 |
606 | /**
607 | * Returns true if autocomplete is defined, false otherwise
608 | *
609 | * @returns {boolean}
610 | */
611 | _exists: function () {
612 | return undefined !== self.$autocomplete;
613 | }
614 | };
615 |
616 | /**
617 | * Update plugin binded input value
618 | *
619 | * @returns {void}
620 | */
621 | self._updateValue = function () {
622 | self.$element.prop('value', self.tags.join(self.KEY_COMMA));
623 | };
624 |
625 | /**
626 | * Define focus events on input
627 | *
628 | * @returns {void}
629 | */
630 | self._focus = function () {
631 | self.$input.on('focus', function () {
632 | self._bindEvent('focus');
633 |
634 | if (self._autocomplete._isSet() && !self.$input.hasClass('is-autocomplete') && !self.$input.hasClass('is-edit')) {
635 | self._autocomplete._build();
636 | self._autocomplete._show();
637 | }
638 | });
639 | };
640 |
641 | /**
642 | * Convert array into object
643 | *
644 | * @param {string[]} arr
645 | * @returns {object}
646 | */
647 | self._toObject = function (arr) {
648 | return arr.reduce(function (o, v, i) {
649 | o[i] = v;
650 | return o;
651 | }, {});
652 | };
653 |
654 | /**
655 | * Input validation
656 | *
657 | * @param {string} value
658 | * @param {boolean} alert
659 | * @returns {boolean}
660 | */
661 | self._validate = function (value, alert) {
662 | var type = '', re;
663 |
664 | switch (true) {
665 | case !value:
666 | case undefined === value:
667 | case 0 === value.length:
668 | self._cancel();
669 | type = 'empty';
670 | break;
671 | case value.length > 0 && value.length < self.options.minLength:
672 | type = 'minLength';
673 | break;
674 | case value.length > self.options.maxLength:
675 | type = 'maxLength';
676 | break;
677 | case self.options.max > 0 && self.tags.length >= self.options.max:
678 | if (!self.$input.hasClass('is-edit')) {
679 | type = 'max';
680 | }
681 | break;
682 | case self.options.email:
683 | re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
684 |
685 | if (!re.test(value)) {
686 | type = 'email';
687 | }
688 | break;
689 | }
690 |
691 | if (type.length > 0) {
692 | return alert ? self._errors(type) : type;
693 | }
694 |
695 | return true;
696 | };
697 |
698 | /**
699 | * Returns true if value is in tags list, false otherwise
700 | *
701 | * @param {string} value
702 | * @returns {boolean}
703 | */
704 | self._exists = function (value) {
705 | return $.inArray(value, self.tags) >= 0;
706 | }
707 |
708 | /**
709 | * Get error message
710 | *
711 | * @param {string} type
712 | * @returns {boolean}
713 | */
714 | self._errors = function (type) {
715 | if (0 === type.length) {
716 | return false;
717 | }
718 |
719 | if (self._autocomplete._exists()) {
720 | self.$autocomplete.remove();
721 | }
722 |
723 | self._displayErrors(self.options.errors[type].replace('%s', self.options[type]), type);
724 |
725 | return false;
726 | };
727 |
728 | /**
729 | * Display errors(s) if any
730 | *
731 | * @param {string} error
732 | * @param {string} type
733 | * @returns {void}
734 | */
735 | self._displayErrors = function (error, type) {
736 | var $error = $(self.ERROR_CONTENT.replace('%s', error)).attr('data-error', type);
737 | var timeout = self.options.errors.timeout;
738 |
739 | if ($('.' + self.ERROR_CLASS + '[data-error="' + type + '"]').length) {
740 | return false;
741 | }
742 |
743 | $error.hide().insertAfter(self.$list).slideDown();
744 |
745 | if (!timeout || timeout <= 0) {
746 | return false;
747 | }
748 |
749 | $('.' + self.ERROR_CLASS).on('click', function () {
750 | self._collapseErrors($(this));
751 | });
752 |
753 | setTimeout(function () {
754 | self._collapseErrors();
755 | }, timeout);
756 | };
757 |
758 | /**
759 | * Clears error(s) if any
760 | *
761 | * @param {object} $elem
762 | * @returns {void}
763 | */
764 | self._collapseErrors = function ($elem) {
765 |
766 | var $obj = $elem ? $elem : $('.' + self.ERROR_CLASS);
767 |
768 | $obj.slideUp(300, function () {
769 | $obj.remove();
770 | });
771 | };
772 |
773 | /**
774 | * Retrieve inputTags instance based on uniqid
775 | *
776 | * @returns {object}
777 | */
778 | self._getInstance = function () {
779 | return window.inputTags.instances[self.UNIQID];
780 | };
781 |
782 | /**
783 | * Store instance based on uniqid
784 | *
785 | * @returns {void}
786 | */
787 | self._setInstance = function () {
788 | window.inputTags.instances[self.UNIQID] = self;
789 | };
790 |
791 | /**
792 | * Returns true if elem exists on options object, false otherwise
793 | *
794 | * @returns {boolean}
795 | */
796 | self._isSet = function (elem) {
797 | return !(undefined === self.options[elem] || false === self.options[elem] || self.options[elem].length);
798 | };
799 |
800 | /**
801 | * Call method based on method_name parameter if exists on options, returns false otherwise
802 | *
803 | * @param {string} method_name
804 | * @param {object} self
805 | * @returns {void}
806 | */
807 | self._callMethod = function (method_name, self) {
808 | if (undefined === self.options[method_name] || 'function' !== typeof self.options[method_name]) {
809 | return false;
810 | }
811 |
812 | self.options[method_name].apply(this, Array.prototype.slice.call(arguments, 1));
813 | }
814 |
815 | /**
816 | * Init an event
817 | *
818 | * @param {string | object} method
819 | * @param {function} callback
820 | * @returns {boolean}
821 | */
822 | self._initEvent = function (method, callback) {
823 | if (!method) {
824 | return false;
825 | }
826 |
827 | switch (typeof method) {
828 | case 'string':
829 | callback(method, self);
830 | break;
831 | case 'object':
832 | method.forEach(function (m, i) {
833 | callback(m, self);
834 | });
835 | break;
836 | }
837 |
838 | return true;
839 | };
840 |
841 | /**
842 | * Bind an event
843 | *
844 | * @param {string | object} method
845 | * @returns {boolean}
846 | */
847 | self._bindEvent = function (method) {
848 | return self._initEvent(method, function (m, s) {
849 | self._callMethod(m, s);
850 | });
851 | };
852 |
853 | /**
854 | * Unbind an event
855 | *
856 | * @param {string | object} method
857 | * @returns {boolean}
858 | */
859 | self._unbindEvent = function (method) {
860 | return self._initEvent(method, function (m) {
861 | self.options[m] = false;
862 | });
863 | };
864 |
865 | self.init();
866 |
867 | self._bindEvent('init');
868 |
869 | self._setInstance(self);
870 | });
871 |
872 | return {
873 | on: function (method, callback) {
874 | window.inputTags.methods.event(method, callback);
875 | }
876 | };
877 |
878 | } else if (window.inputTags.methods[options]) {
879 | var id = $(this).attr('data-uniqid');
880 | var _instance = window.inputTags.instances[id];
881 |
882 | if (undefined === _instance) {
883 | return $.error("[undefined instance] No inputTags instance found.");
884 | }
885 |
886 | return window.inputTags.methods[options].apply(this, Array.prototype.slice.call(arguments, 1));
887 | } else {
888 | $.error("[undefined method] The method [" + options + "] does not exists.");
889 | }
890 | };
891 |
892 | $.fn.inputTags.defaults = {
893 | tags: [],
894 | keys: [],
895 | minLength: 2,
896 | maxLength: 30,
897 | max: 6,
898 | email: false,
899 | only: true,
900 | init: false,
901 | create: false,
902 | update: false,
903 | destroy: false,
904 | focus: false,
905 | selected: false,
906 | unselected: false,
907 | change: false,
908 | autocompleteTagSelect: false,
909 | editable: true,
910 | autocomplete: {
911 | values: [],
912 | only: false
913 | },
914 | errors: {
915 | empty: 'Attention, vous ne pouvez pas ajouter un tag vide.',
916 | minLength: 'Attention, votre tag doit avoir au minimum %s caractères.',
917 | maxLength: 'Attention, votre tag ne doit pas dépasser %s caractères.',
918 | max: 'Attention, le nombre de tags ne doit pas dépasser %s.',
919 | email: 'Attention, l\'adresse email que vous avez entré n\'est pas valide',
920 | exists: 'Attention, ce tag existe déjà !',
921 | autocomplete_only: 'Attention, vous devez sélectionner une valeur dans la liste.',
922 | timeout: 8000
923 | }
924 | };
925 |
926 | })(jQuery);
927 |
--------------------------------------------------------------------------------