taggle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/taggle-full.js +3 -0
- data/app/assets/javascripts/taggle-ie8.js +218 -0
- data/app/assets/javascripts/taggle-ie9.js +176 -0
- data/app/assets/javascripts/taggle.js +890 -0
- data/app/assets/stylesheets/taggle.scss +6 -0
- data/app/assets/stylesheets/taggle/autocomplete.scss +58 -0
- data/app/assets/stylesheets/taggle/basics.scss +127 -0
- data/app/assets/stylesheets/taggle/custom-tags.scss +96 -0
- data/app/assets/stylesheets/taggle/duplicate-tag-animations.scss +81 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/taggle.rb +7 -0
- data/lib/taggle/engine.rb +5 -0
- data/lib/taggle/version.rb +3 -0
- data/taggle.gemspec +35 -0
- metadata +125 -0
@@ -0,0 +1,890 @@
|
|
1
|
+
/* !
|
2
|
+
* @author Sean Coker <sean@seancoker.com>
|
3
|
+
* @version 1.11.1
|
4
|
+
* @url http://sean.is/poppin/tags
|
5
|
+
* @license MIT
|
6
|
+
* @description Taggle is a dependency-less tagging library
|
7
|
+
*/
|
8
|
+
|
9
|
+
(function(root, factory) {
|
10
|
+
'use strict';
|
11
|
+
var libName = 'Taggle';
|
12
|
+
|
13
|
+
/* global define, module */
|
14
|
+
if (typeof define === 'function' && define.amd) {
|
15
|
+
define([], function() {
|
16
|
+
var module = factory();
|
17
|
+
root[libName] = module;
|
18
|
+
return module;
|
19
|
+
});
|
20
|
+
}
|
21
|
+
else if (typeof module === 'object' && module.exports) {
|
22
|
+
module.exports = root[libName] = factory();
|
23
|
+
}
|
24
|
+
else {
|
25
|
+
root[libName] = factory();
|
26
|
+
}
|
27
|
+
}(this, function() {
|
28
|
+
'use strict';
|
29
|
+
/////////////////////
|
30
|
+
// Default options //
|
31
|
+
/////////////////////
|
32
|
+
|
33
|
+
var noop = function() {};
|
34
|
+
var retTrue = function() {
|
35
|
+
return true;
|
36
|
+
};
|
37
|
+
var BACKSPACE = 8;
|
38
|
+
var COMMA = 188;
|
39
|
+
var TAB = 9;
|
40
|
+
var ENTER = 13;
|
41
|
+
|
42
|
+
var DEFAULTS = {
|
43
|
+
/**
|
44
|
+
* Class names to be added on each tag entered
|
45
|
+
* @type {String}
|
46
|
+
*/
|
47
|
+
additionalTagClasses: '',
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Allow duplicate tags to be entered in the field?
|
51
|
+
* @type {Boolean}
|
52
|
+
*/
|
53
|
+
allowDuplicates: false,
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Allow the saving of a tag on blur, rather than it being
|
57
|
+
* removed.
|
58
|
+
*
|
59
|
+
* @type {Boolean}
|
60
|
+
*/
|
61
|
+
saveOnBlur: false,
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Clear the input value when blurring.
|
65
|
+
*
|
66
|
+
* @type {Boolean}
|
67
|
+
*/
|
68
|
+
clearOnBlur: true,
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Class name that will be added onto duplicate existant tag
|
72
|
+
* @type {String}
|
73
|
+
* @todo
|
74
|
+
* @deprecated can be handled by onBeforeTagAdd
|
75
|
+
*/
|
76
|
+
duplicateTagClass: '',
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Class added to the container div when focused
|
80
|
+
* @type {String}
|
81
|
+
*/
|
82
|
+
containerFocusClass: 'active',
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Should the input be focused when the container is clicked?
|
86
|
+
* @type {Bool}
|
87
|
+
*/
|
88
|
+
focusInputOnContainerClick: true,
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Name added to the hidden inputs within each tag
|
92
|
+
* @type {String}
|
93
|
+
*/
|
94
|
+
hiddenInputName: 'taggles[]',
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Tags that should be preloaded in the div on load
|
98
|
+
* @type {Array}
|
99
|
+
*/
|
100
|
+
tags: [],
|
101
|
+
|
102
|
+
/**
|
103
|
+
* The default delimeter character to split tags on
|
104
|
+
* @type {String}
|
105
|
+
*/
|
106
|
+
delimeter: ',',
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Add an ID to each of the tags.
|
110
|
+
* @type {Boolean}
|
111
|
+
* @todo
|
112
|
+
* @deprecated make this the default in next version
|
113
|
+
*/
|
114
|
+
attachTagId: false,
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Tags that the user will be restricted to
|
118
|
+
* @type {Array}
|
119
|
+
*/
|
120
|
+
allowedTags: [],
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Tags that the user will not be able to add
|
124
|
+
* @type {Array}
|
125
|
+
*/
|
126
|
+
disallowedTags: [],
|
127
|
+
|
128
|
+
/**
|
129
|
+
* Limit the number of tags that can be added
|
130
|
+
* @type {Number}
|
131
|
+
*/
|
132
|
+
maxTags: null,
|
133
|
+
|
134
|
+
/**
|
135
|
+
* If within a form, you can specify the tab index flow
|
136
|
+
* @type {Number}
|
137
|
+
*/
|
138
|
+
tabIndex: 1,
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Placeholder string to be placed in an empty taggle field
|
142
|
+
* @type {String}
|
143
|
+
*/
|
144
|
+
placeholder: 'Enter tags...',
|
145
|
+
|
146
|
+
/**
|
147
|
+
* Keycodes that will add a tag
|
148
|
+
* @type {Array}
|
149
|
+
*/
|
150
|
+
submitKeys: [COMMA, TAB, ENTER],
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Preserve case of tags being added ie
|
154
|
+
* "tag" is different than "Tag"
|
155
|
+
* @type {Boolean}
|
156
|
+
*/
|
157
|
+
preserveCase: false,
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Function hook called with the to-be-added input DOM element.
|
161
|
+
*
|
162
|
+
* @param {HTMLElement} li The list item to be added
|
163
|
+
*/
|
164
|
+
inputFormatter: noop,
|
165
|
+
|
166
|
+
/**
|
167
|
+
* Function hook called with the to-be-added tag DOM element.
|
168
|
+
* Use this function to edit the list item before it is appended
|
169
|
+
* to the DOM
|
170
|
+
* @param {HTMLElement} li The list item to be added
|
171
|
+
*/
|
172
|
+
tagFormatter: noop,
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Function hook called before a tag is added. Return false
|
176
|
+
* to prevent tag from being added
|
177
|
+
* @param {String} tag The tag to be added
|
178
|
+
*/
|
179
|
+
onBeforeTagAdd: noop,
|
180
|
+
|
181
|
+
/**
|
182
|
+
* Function hook called when a tag is added
|
183
|
+
* @param {Event} event Event triggered when tag was added
|
184
|
+
* @param {String} tag The tag added
|
185
|
+
*/
|
186
|
+
onTagAdd: noop,
|
187
|
+
|
188
|
+
/**
|
189
|
+
* Function hook called before a tag is removed. Return false
|
190
|
+
* to prevent tag from being removed
|
191
|
+
* @param {String} tag The tag to be removed
|
192
|
+
*/
|
193
|
+
onBeforeTagRemove: retTrue,
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Function hook called when a tag is removed
|
197
|
+
* @param {Event} event Event triggered when tag was removed
|
198
|
+
* @param {String} tag The tag removed
|
199
|
+
*/
|
200
|
+
onTagRemove: noop
|
201
|
+
};
|
202
|
+
|
203
|
+
//////////////////////
|
204
|
+
// Helper functions //
|
205
|
+
//////////////////////
|
206
|
+
|
207
|
+
function _extend() {
|
208
|
+
var master = arguments[0];
|
209
|
+
for (var i = 1, l = arguments.length; i < l; i++) {
|
210
|
+
var object = arguments[i];
|
211
|
+
for (var key in object) {
|
212
|
+
if (object.hasOwnProperty(key)) {
|
213
|
+
master[key] = object[key];
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
return master;
|
219
|
+
}
|
220
|
+
|
221
|
+
function _isArray(arr) {
|
222
|
+
if (Array.isArray) {
|
223
|
+
return Array.isArray(arr);
|
224
|
+
}
|
225
|
+
return Object.prototype.toString.call(arr) === '[object Array]';
|
226
|
+
}
|
227
|
+
|
228
|
+
function _on(element, eventName, handler) {
|
229
|
+
if (element.addEventListener) {
|
230
|
+
element.addEventListener(eventName, handler, false);
|
231
|
+
}
|
232
|
+
else if (element.attachEvent) {
|
233
|
+
element.attachEvent('on' + eventName, handler);
|
234
|
+
}
|
235
|
+
else {
|
236
|
+
element['on' + eventName] = handler;
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
function _trim(str) {
|
241
|
+
return str.replace(/^\s+|\s+$/g, '');
|
242
|
+
}
|
243
|
+
|
244
|
+
function _setText(el, text) {
|
245
|
+
if (window.attachEvent && !window.addEventListener) { // <= IE8
|
246
|
+
el.innerText = text;
|
247
|
+
}
|
248
|
+
else {
|
249
|
+
el.textContent = text;
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
/**
|
254
|
+
* Constructor
|
255
|
+
* @param {Mixed} el ID of an element or the actual element
|
256
|
+
* @param {Object} options
|
257
|
+
*/
|
258
|
+
var Taggle = function(el, options) {
|
259
|
+
this.settings = _extend({}, DEFAULTS, options);
|
260
|
+
this.measurements = {
|
261
|
+
container: {
|
262
|
+
rect: null,
|
263
|
+
style: null,
|
264
|
+
padding: null
|
265
|
+
}
|
266
|
+
};
|
267
|
+
this.container = el;
|
268
|
+
this.tag = {
|
269
|
+
values: [],
|
270
|
+
elements: []
|
271
|
+
};
|
272
|
+
this.list = document.createElement('ul');
|
273
|
+
this.inputLi = document.createElement('li');
|
274
|
+
this.input = document.createElement('input');
|
275
|
+
this.sizer = document.createElement('div');
|
276
|
+
this.pasting = false;
|
277
|
+
this.placeholder = null;
|
278
|
+
|
279
|
+
if (this.settings.placeholder) {
|
280
|
+
this.placeholder = document.createElement('span');
|
281
|
+
}
|
282
|
+
|
283
|
+
if (typeof el === 'string') {
|
284
|
+
this.container = document.getElementById(el);
|
285
|
+
}
|
286
|
+
|
287
|
+
this._id = 0;
|
288
|
+
this._setMeasurements();
|
289
|
+
this._setupTextarea();
|
290
|
+
this._attachEvents();
|
291
|
+
};
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Gets all the layout measurements up front
|
295
|
+
*/
|
296
|
+
Taggle.prototype._setMeasurements = function() {
|
297
|
+
this.measurements.container.rect = this.container.getBoundingClientRect();
|
298
|
+
this.measurements.container.style = window.getComputedStyle(this.container);
|
299
|
+
|
300
|
+
var style = this.measurements.container.style;
|
301
|
+
var lpad = parseInt(style['padding-left'] || style.paddingLeft, 10);
|
302
|
+
var rpad = parseInt(style['padding-right'] || style.paddingRight, 10);
|
303
|
+
var lborder = parseInt(style['border-left-width'] || style.borderLeftWidth, 10);
|
304
|
+
var rborder = parseInt(style['border-right-width'] || style.borderRightWidth, 10);
|
305
|
+
|
306
|
+
this.measurements.container.padding = lpad + rpad + lborder + rborder;
|
307
|
+
};
|
308
|
+
|
309
|
+
/**
|
310
|
+
* Setup the div container for tags to be entered
|
311
|
+
*/
|
312
|
+
Taggle.prototype._setupTextarea = function() {
|
313
|
+
var fontSize;
|
314
|
+
|
315
|
+
this.list.className = 'taggle_list';
|
316
|
+
this.input.type = 'text';
|
317
|
+
// Make sure no left/right padding messes with the input sizing
|
318
|
+
this.input.style.paddingLeft = 0;
|
319
|
+
this.input.style.paddingRight = 0;
|
320
|
+
this.input.className = 'taggle_input';
|
321
|
+
this.input.tabIndex = this.settings.tabIndex;
|
322
|
+
this.sizer.className = 'taggle_sizer';
|
323
|
+
|
324
|
+
if (this.settings.tags.length) {
|
325
|
+
for (var i = 0, len = this.settings.tags.length; i < len; i++) {
|
326
|
+
var taggle = this._createTag(this.settings.tags[i]);
|
327
|
+
this.list.appendChild(taggle);
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
if (this.placeholder) {
|
332
|
+
this.placeholder.style.opacity = 0;
|
333
|
+
this.placeholder.classList.add('taggle_placeholder');
|
334
|
+
this.container.appendChild(this.placeholder);
|
335
|
+
_setText(this.placeholder, this.settings.placeholder);
|
336
|
+
|
337
|
+
if (!this.settings.tags.length) {
|
338
|
+
this.placeholder.style.opacity = 1;
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
var formattedInput = this.settings.inputFormatter(this.input);
|
343
|
+
if (formattedInput) {
|
344
|
+
this.input = formattedInput;
|
345
|
+
}
|
346
|
+
|
347
|
+
this.inputLi.appendChild(this.input);
|
348
|
+
this.list.appendChild(this.inputLi);
|
349
|
+
this.container.appendChild(this.list);
|
350
|
+
this.container.appendChild(this.sizer);
|
351
|
+
fontSize = window.getComputedStyle(this.input).fontSize;
|
352
|
+
this.sizer.style.fontSize = fontSize;
|
353
|
+
};
|
354
|
+
|
355
|
+
/**
|
356
|
+
* Attaches neccessary events
|
357
|
+
*/
|
358
|
+
Taggle.prototype._attachEvents = function() {
|
359
|
+
var self = this;
|
360
|
+
|
361
|
+
if (this.settings.focusInputOnContainerClick) {
|
362
|
+
_on(this.container, 'click', function() {
|
363
|
+
self.input.focus();
|
364
|
+
});
|
365
|
+
}
|
366
|
+
|
367
|
+
_on(this.input, 'focus', this._focusInput.bind(this));
|
368
|
+
_on(this.input, 'blur', this._blurEvent.bind(this));
|
369
|
+
_on(this.input, 'keydown', this._keydownEvents.bind(this));
|
370
|
+
_on(this.input, 'keyup', this._keyupEvents.bind(this));
|
371
|
+
};
|
372
|
+
|
373
|
+
/**
|
374
|
+
* Resizes the hidden input where user types to fill in the
|
375
|
+
* width of the div
|
376
|
+
*/
|
377
|
+
Taggle.prototype._fixInputWidth = function() {
|
378
|
+
var width;
|
379
|
+
var inputRect;
|
380
|
+
var rect;
|
381
|
+
var leftPos;
|
382
|
+
var padding;
|
383
|
+
|
384
|
+
this._setMeasurements();
|
385
|
+
|
386
|
+
// Reset width incase we've broken to the next line on a backspace erase
|
387
|
+
this._setInputWidth();
|
388
|
+
|
389
|
+
inputRect = this.input.getBoundingClientRect();
|
390
|
+
rect = this.measurements.container.rect;
|
391
|
+
width = ~~rect.width;
|
392
|
+
// Could probably just use right - left all the time
|
393
|
+
// but eh, this check is mostly for IE8
|
394
|
+
if (!width) {
|
395
|
+
width = ~~rect.right - ~~rect.left;
|
396
|
+
}
|
397
|
+
leftPos = ~~inputRect.left - ~~rect.left;
|
398
|
+
padding = this.measurements.container.padding;
|
399
|
+
|
400
|
+
this._setInputWidth(width - leftPos - padding);
|
401
|
+
};
|
402
|
+
|
403
|
+
/**
|
404
|
+
* Returns whether or not the specified tag text can be added
|
405
|
+
* @param {Event} e event causing the potentially added tag
|
406
|
+
* @param {String} text tag value
|
407
|
+
* @return {Boolean}
|
408
|
+
*/
|
409
|
+
Taggle.prototype._canAdd = function(e, text) {
|
410
|
+
if (!text) {
|
411
|
+
return false;
|
412
|
+
}
|
413
|
+
var limit = this.settings.maxTags;
|
414
|
+
if (limit !== null && limit <= this.getTagValues().length) {
|
415
|
+
return false;
|
416
|
+
}
|
417
|
+
|
418
|
+
if (this.settings.onBeforeTagAdd(e, text) === false) {
|
419
|
+
return false;
|
420
|
+
}
|
421
|
+
|
422
|
+
if (!this.settings.allowDuplicates && this._hasDupes(text)) {
|
423
|
+
return false;
|
424
|
+
}
|
425
|
+
|
426
|
+
var sensitive = this.settings.preserveCase;
|
427
|
+
var allowed = this.settings.allowedTags;
|
428
|
+
|
429
|
+
if (allowed.length && !this._tagIsInArray(text, allowed, sensitive)) {
|
430
|
+
return false;
|
431
|
+
}
|
432
|
+
|
433
|
+
var disallowed = this.settings.disallowedTags;
|
434
|
+
if (disallowed.length && this._tagIsInArray(text, disallowed, sensitive)) {
|
435
|
+
return false;
|
436
|
+
}
|
437
|
+
|
438
|
+
return true;
|
439
|
+
};
|
440
|
+
|
441
|
+
/**
|
442
|
+
* Returns whether a string is in an array based on case sensitivity
|
443
|
+
*
|
444
|
+
* @param {String} text string to search for
|
445
|
+
* @param {Array} arr array of strings to search through
|
446
|
+
* @param {Boolean} caseSensitive
|
447
|
+
* @return {Boolean}
|
448
|
+
*/
|
449
|
+
Taggle.prototype._tagIsInArray = function(text, arr, caseSensitive) {
|
450
|
+
if (caseSensitive) {
|
451
|
+
return arr.indexOf(text) !== -1;
|
452
|
+
}
|
453
|
+
|
454
|
+
var lowercased = [].slice.apply(arr).map(function(str) {
|
455
|
+
return str.toLowerCase();
|
456
|
+
});
|
457
|
+
|
458
|
+
return lowercased.indexOf(text) !== -1;
|
459
|
+
};
|
460
|
+
|
461
|
+
/**
|
462
|
+
* Appends tag with its corresponding input to the list
|
463
|
+
* @param {Event} e
|
464
|
+
* @param {String} text
|
465
|
+
*/
|
466
|
+
Taggle.prototype._add = function(e, text) {
|
467
|
+
var self = this;
|
468
|
+
var values = text || '';
|
469
|
+
|
470
|
+
if (typeof text !== 'string') {
|
471
|
+
values = _trim(this.input.value);
|
472
|
+
}
|
473
|
+
|
474
|
+
values.split(this.settings.delimeter).map(function(val) {
|
475
|
+
return self._formatTag(val);
|
476
|
+
}).forEach(function(val) {
|
477
|
+
if (!self._canAdd(e, val)) {
|
478
|
+
return;
|
479
|
+
}
|
480
|
+
|
481
|
+
var li = self._createTag(val);
|
482
|
+
var lis = self.list.children;
|
483
|
+
var lastLi = lis[lis.length - 1];
|
484
|
+
self.list.insertBefore(li, lastLi);
|
485
|
+
|
486
|
+
|
487
|
+
val = self.tag.values[self.tag.values.length - 1];
|
488
|
+
|
489
|
+
self.settings.onTagAdd(e, val);
|
490
|
+
|
491
|
+
self.input.value = '';
|
492
|
+
self._fixInputWidth();
|
493
|
+
self._focusInput();
|
494
|
+
});
|
495
|
+
};
|
496
|
+
|
497
|
+
/**
|
498
|
+
* Removes last tag if it has already been probed
|
499
|
+
* @param {Event} e
|
500
|
+
*/
|
501
|
+
Taggle.prototype._checkLastTag = function(e) {
|
502
|
+
e = e || window.event;
|
503
|
+
|
504
|
+
var taggles = this.container.querySelectorAll('.taggle');
|
505
|
+
var lastTaggle = taggles[taggles.length - 1];
|
506
|
+
var hotClass = 'taggle_hot';
|
507
|
+
var heldDown = this.input.classList.contains('taggle_back');
|
508
|
+
|
509
|
+
// prevent holding backspace from deleting all tags
|
510
|
+
if (this.input.value === '' && e.keyCode === BACKSPACE && !heldDown) {
|
511
|
+
if (lastTaggle.classList.contains(hotClass)) {
|
512
|
+
this.input.classList.add('taggle_back');
|
513
|
+
this._remove(lastTaggle, e);
|
514
|
+
this._fixInputWidth();
|
515
|
+
this._focusInput();
|
516
|
+
}
|
517
|
+
else {
|
518
|
+
lastTaggle.classList.add(hotClass);
|
519
|
+
}
|
520
|
+
}
|
521
|
+
else if (lastTaggle.classList.contains(hotClass)) {
|
522
|
+
lastTaggle.classList.remove(hotClass);
|
523
|
+
}
|
524
|
+
};
|
525
|
+
|
526
|
+
/**
|
527
|
+
* Setter for the hidden input.
|
528
|
+
* @param {Number} width
|
529
|
+
*/
|
530
|
+
Taggle.prototype._setInputWidth = function(width) {
|
531
|
+
this.input.style.width = (width || 10) + 'px';
|
532
|
+
};
|
533
|
+
|
534
|
+
/**
|
535
|
+
* Checks global tags array if provided tag exists
|
536
|
+
* @param {String} text
|
537
|
+
* @return {Boolean}
|
538
|
+
*/
|
539
|
+
Taggle.prototype._hasDupes = function(text) {
|
540
|
+
var needle = this.tag.values.indexOf(text);
|
541
|
+
var tagglelist = this.container.querySelector('.taggle_list');
|
542
|
+
var dupes;
|
543
|
+
|
544
|
+
if (this.settings.duplicateTagClass) {
|
545
|
+
dupes = tagglelist.querySelectorAll('.' + this.settings.duplicateTagClass);
|
546
|
+
for (var i = 0, len = dupes.length; i < len; i++) {
|
547
|
+
dupes[i].classList.remove(this.settings.duplicateTagClass);
|
548
|
+
}
|
549
|
+
}
|
550
|
+
|
551
|
+
// if found
|
552
|
+
if (needle > -1) {
|
553
|
+
if (this.settings.duplicateTagClass) {
|
554
|
+
tagglelist.childNodes[needle].classList.add(this.settings.duplicateTagClass);
|
555
|
+
}
|
556
|
+
return true;
|
557
|
+
}
|
558
|
+
|
559
|
+
return false;
|
560
|
+
};
|
561
|
+
|
562
|
+
/**
|
563
|
+
* Checks whether or not the key pressed is acceptable
|
564
|
+
* @param {Number} key code
|
565
|
+
* @return {Boolean}
|
566
|
+
*/
|
567
|
+
Taggle.prototype._isConfirmKey = function(key) {
|
568
|
+
var confirmKey = false;
|
569
|
+
|
570
|
+
if (this.settings.submitKeys.indexOf(key) > -1) {
|
571
|
+
confirmKey = true;
|
572
|
+
}
|
573
|
+
|
574
|
+
return confirmKey;
|
575
|
+
};
|
576
|
+
|
577
|
+
// Event handlers
|
578
|
+
|
579
|
+
/**
|
580
|
+
* Handles focus state of div container.
|
581
|
+
*/
|
582
|
+
Taggle.prototype._focusInput = function() {
|
583
|
+
this._fixInputWidth();
|
584
|
+
|
585
|
+
if (!this.container.classList.contains(this.settings.containerFocusClass)) {
|
586
|
+
this.container.classList.add(this.settings.containerFocusClass);
|
587
|
+
}
|
588
|
+
|
589
|
+
if (this.placeholder) {
|
590
|
+
this.placeholder.style.opacity = 0;
|
591
|
+
}
|
592
|
+
};
|
593
|
+
|
594
|
+
/**
|
595
|
+
* Runs all the events that need to happen on a blur
|
596
|
+
* @param {Event} e
|
597
|
+
*/
|
598
|
+
Taggle.prototype._blurEvent = function(e) {
|
599
|
+
if (this.container.classList.contains(this.settings.containerFocusClass)) {
|
600
|
+
this.container.classList.remove(this.settings.containerFocusClass);
|
601
|
+
}
|
602
|
+
|
603
|
+
if (this.settings.saveOnBlur) {
|
604
|
+
e = e || window.event;
|
605
|
+
|
606
|
+
this._listenForEndOfContainer();
|
607
|
+
|
608
|
+
if (this.input.value !== '') {
|
609
|
+
this._confirmValidTagEvent(e);
|
610
|
+
return;
|
611
|
+
}
|
612
|
+
|
613
|
+
if (this.tag.values.length) {
|
614
|
+
this._checkLastTag(e);
|
615
|
+
}
|
616
|
+
}
|
617
|
+
else if (this.settings.clearOnBlur) {
|
618
|
+
this.input.value = '';
|
619
|
+
this._setInputWidth();
|
620
|
+
}
|
621
|
+
|
622
|
+
if (!this.tag.values.length && this.placeholder && !this.input.value) {
|
623
|
+
this.placeholder.style.opacity = 1;
|
624
|
+
}
|
625
|
+
};
|
626
|
+
|
627
|
+
/**
|
628
|
+
* Runs all the events that need to run on keydown
|
629
|
+
* @param {Event} e
|
630
|
+
*/
|
631
|
+
Taggle.prototype._keydownEvents = function(e) {
|
632
|
+
e = e || window.event;
|
633
|
+
|
634
|
+
var key = e.keyCode;
|
635
|
+
this.pasting = false;
|
636
|
+
|
637
|
+
this._listenForEndOfContainer();
|
638
|
+
|
639
|
+
if (key === 86 && e.metaKey) {
|
640
|
+
this.pasting = true;
|
641
|
+
}
|
642
|
+
|
643
|
+
if (this._isConfirmKey(key) && this.input.value !== '') {
|
644
|
+
this._confirmValidTagEvent(e);
|
645
|
+
return;
|
646
|
+
}
|
647
|
+
|
648
|
+
if (this.tag.values.length) {
|
649
|
+
this._checkLastTag(e);
|
650
|
+
}
|
651
|
+
};
|
652
|
+
|
653
|
+
/**
|
654
|
+
* Runs all the events that need to run on keyup
|
655
|
+
* @param {Event} e
|
656
|
+
*/
|
657
|
+
Taggle.prototype._keyupEvents = function(e) {
|
658
|
+
e = e || window.event;
|
659
|
+
|
660
|
+
this.input.classList.remove('taggle_back');
|
661
|
+
|
662
|
+
_setText(this.sizer, this.input.value);
|
663
|
+
|
664
|
+
if (this.pasting && this.input.value !== '') {
|
665
|
+
this._add(e);
|
666
|
+
this.pasting = false;
|
667
|
+
}
|
668
|
+
};
|
669
|
+
|
670
|
+
/**
|
671
|
+
* Confirms the inputted value to be converted to a tag
|
672
|
+
* @param {Event} e
|
673
|
+
*/
|
674
|
+
Taggle.prototype._confirmValidTagEvent = function(e) {
|
675
|
+
e = e || window.event;
|
676
|
+
|
677
|
+
// prevents from jumping out of textarea
|
678
|
+
if (e.preventDefault) {
|
679
|
+
e.preventDefault();
|
680
|
+
}
|
681
|
+
else {
|
682
|
+
e.returnValue = false;
|
683
|
+
}
|
684
|
+
|
685
|
+
this._add(e);
|
686
|
+
};
|
687
|
+
|
688
|
+
/**
|
689
|
+
* Approximates when the hidden input should break to the next line
|
690
|
+
*/
|
691
|
+
Taggle.prototype._listenForEndOfContainer = function() {
|
692
|
+
var width = this.sizer.getBoundingClientRect().width;
|
693
|
+
var max = this.measurements.container.rect.width - this.measurements.container.padding;
|
694
|
+
var size = parseInt(this.sizer.style.fontSize, 10);
|
695
|
+
|
696
|
+
// 1.5 just seems to be a good multiplier here
|
697
|
+
if (width + (size * 1.5) > parseInt(this.input.style.width, 10)) {
|
698
|
+
this.input.style.width = max + 'px';
|
699
|
+
}
|
700
|
+
};
|
701
|
+
|
702
|
+
Taggle.prototype._createTag = function(text) {
|
703
|
+
var li = document.createElement('li');
|
704
|
+
var close = document.createElement('button');
|
705
|
+
var hidden = document.createElement('input');
|
706
|
+
var span = document.createElement('span');
|
707
|
+
|
708
|
+
text = this._formatTag(text);
|
709
|
+
|
710
|
+
close.innerHTML = '×';
|
711
|
+
close.className = 'close';
|
712
|
+
close.type = 'button';
|
713
|
+
_on(close, 'click', this._remove.bind(this, close));
|
714
|
+
|
715
|
+
_setText(span, text);
|
716
|
+
span.className = 'taggle_text';
|
717
|
+
|
718
|
+
li.className = 'taggle ' + this.settings.additionalTagClasses;
|
719
|
+
|
720
|
+
hidden.type = 'hidden';
|
721
|
+
hidden.value = text;
|
722
|
+
hidden.name = this.settings.hiddenInputName;
|
723
|
+
|
724
|
+
li.appendChild(span);
|
725
|
+
li.appendChild(close);
|
726
|
+
li.appendChild(hidden);
|
727
|
+
|
728
|
+
var formatted = this.settings.tagFormatter(li);
|
729
|
+
|
730
|
+
if (typeof formatted !== 'undefined') {
|
731
|
+
li = formatted;
|
732
|
+
}
|
733
|
+
|
734
|
+
if (!(li instanceof HTMLElement) || li.tagName !== 'LI') {
|
735
|
+
throw new Error('tagFormatter must return an li element');
|
736
|
+
}
|
737
|
+
|
738
|
+
if (this.settings.attachTagId) {
|
739
|
+
this._id += 1;
|
740
|
+
text = {
|
741
|
+
text: text,
|
742
|
+
id: this._id
|
743
|
+
};
|
744
|
+
}
|
745
|
+
|
746
|
+
this.tag.values.push(text);
|
747
|
+
this.tag.elements.push(li);
|
748
|
+
|
749
|
+
return li;
|
750
|
+
};
|
751
|
+
|
752
|
+
/**
|
753
|
+
* Removes tag from the tags collection
|
754
|
+
* @param {li} li List item to remove
|
755
|
+
* @param {Event} e
|
756
|
+
*/
|
757
|
+
Taggle.prototype._remove = function(li, e) {
|
758
|
+
var self = this;
|
759
|
+
var text;
|
760
|
+
var elem;
|
761
|
+
var index;
|
762
|
+
|
763
|
+
if (li.tagName.toLowerCase() !== 'li') {
|
764
|
+
li = li.parentNode;
|
765
|
+
}
|
766
|
+
|
767
|
+
elem = (li.tagName.toLowerCase() === 'a') ? li.parentNode : li;
|
768
|
+
index = this.tag.elements.indexOf(elem);
|
769
|
+
|
770
|
+
text = this.tag.values[index];
|
771
|
+
|
772
|
+
function done(error) {
|
773
|
+
if (error) {
|
774
|
+
return;
|
775
|
+
}
|
776
|
+
|
777
|
+
li.parentNode.removeChild(li);
|
778
|
+
|
779
|
+
// Going to assume the indicies match for now
|
780
|
+
self.tag.elements.splice(index, 1);
|
781
|
+
self.tag.values.splice(index, 1);
|
782
|
+
|
783
|
+
self.settings.onTagRemove(e, text);
|
784
|
+
|
785
|
+
self._focusInput();
|
786
|
+
}
|
787
|
+
|
788
|
+
var ret = this.settings.onBeforeTagRemove(e, text, done);
|
789
|
+
|
790
|
+
if (!ret) {
|
791
|
+
return;
|
792
|
+
}
|
793
|
+
|
794
|
+
done();
|
795
|
+
};
|
796
|
+
|
797
|
+
/**
|
798
|
+
* Format the text for a tag
|
799
|
+
* @param {String} text Tag text
|
800
|
+
* @return {String}
|
801
|
+
*/
|
802
|
+
Taggle.prototype._formatTag = function(text) {
|
803
|
+
return this.settings.preserveCase ? text : text.toLowerCase();
|
804
|
+
};
|
805
|
+
|
806
|
+
Taggle.prototype.getTags = function() {
|
807
|
+
return {
|
808
|
+
elements: this.getTagElements(),
|
809
|
+
values: this.getTagValues()
|
810
|
+
};
|
811
|
+
};
|
812
|
+
|
813
|
+
// @todo
|
814
|
+
// @deprecated use getTags().elements
|
815
|
+
Taggle.prototype.getTagElements = function() {
|
816
|
+
return this.tag.elements;
|
817
|
+
};
|
818
|
+
|
819
|
+
// @todo
|
820
|
+
// @deprecated use getTags().values
|
821
|
+
Taggle.prototype.getTagValues = function() {
|
822
|
+
return [].slice.apply(this.tag.values);
|
823
|
+
};
|
824
|
+
|
825
|
+
Taggle.prototype.getInput = function() {
|
826
|
+
return this.input;
|
827
|
+
};
|
828
|
+
|
829
|
+
Taggle.prototype.getContainer = function() {
|
830
|
+
return this.container;
|
831
|
+
};
|
832
|
+
|
833
|
+
Taggle.prototype.add = function(text) {
|
834
|
+
var isArr = _isArray(text);
|
835
|
+
|
836
|
+
if (isArr) {
|
837
|
+
for (var i = 0, len = text.length; i < len; i++) {
|
838
|
+
if (typeof text[i] === 'string') {
|
839
|
+
this._add(null, text[i]);
|
840
|
+
}
|
841
|
+
}
|
842
|
+
}
|
843
|
+
else {
|
844
|
+
this._add(null, text);
|
845
|
+
}
|
846
|
+
|
847
|
+
return this;
|
848
|
+
};
|
849
|
+
|
850
|
+
Taggle.prototype.remove = function(text, all) {
|
851
|
+
var len = this.tag.values.length - 1;
|
852
|
+
var found = false;
|
853
|
+
|
854
|
+
while (len > -1) {
|
855
|
+
var tagText = this.tag.values[len];
|
856
|
+
if (this.settings.attachTagId) {
|
857
|
+
tagText = tagText.text;
|
858
|
+
}
|
859
|
+
|
860
|
+
if (tagText === text) {
|
861
|
+
found = true;
|
862
|
+
this._remove(this.tag.elements[len]);
|
863
|
+
}
|
864
|
+
|
865
|
+
if (found && !all) {
|
866
|
+
break;
|
867
|
+
}
|
868
|
+
|
869
|
+
len--;
|
870
|
+
}
|
871
|
+
|
872
|
+
return this;
|
873
|
+
};
|
874
|
+
|
875
|
+
Taggle.prototype.removeAll = function() {
|
876
|
+
for (var i = this.tag.values.length - 1; i >= 0; i--) {
|
877
|
+
this._remove(this.tag.elements[i]);
|
878
|
+
}
|
879
|
+
|
880
|
+
return this;
|
881
|
+
};
|
882
|
+
|
883
|
+
Taggle.prototype.setOptions = function(options) {
|
884
|
+
this.settings = _extend({}, this.settings, options || {});
|
885
|
+
|
886
|
+
return this;
|
887
|
+
};
|
888
|
+
|
889
|
+
return Taggle;
|
890
|
+
}));
|