bard-tag_field 0.5.0 → 0.5.2

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.
@@ -0,0 +1,546 @@
1
+ /**
2
+ * Taggle - dependency-less tagging library
3
+ * @author Sean Coker <hello@sean.is>
4
+ * @version 1.15.0 (modified)
5
+ * @license MIT
6
+ */
7
+
8
+ /////////////////////
9
+ // Default options //
10
+ /////////////////////
11
+
12
+ const BACKSPACE = 8;
13
+ const DELETE = 46;
14
+ const COMMA = 188;
15
+ const TAB = 9;
16
+ const ENTER = 13;
17
+
18
+ const DEFAULTS = {
19
+ /**
20
+ * Class added to the container div when focused
21
+ * @type {String}
22
+ */
23
+ containerFocusClass: 'active',
24
+
25
+ /**
26
+ * Spaces will be removed from the tags by default
27
+ * @type {Boolean}
28
+ */
29
+ trimTags: true,
30
+
31
+ /**
32
+ * Limit the number of tags that can be added
33
+ * @type {Number}
34
+ */
35
+ maxTags: null,
36
+
37
+ /**
38
+ * Placeholder string to be placed in an empty taggle field
39
+ * @type {String}
40
+ */
41
+ placeholder: 'Enter tags...',
42
+
43
+ /**
44
+ * Keycodes that will add a tag
45
+ * @type {Array}
46
+ */
47
+ submitKeys: [COMMA, TAB, ENTER],
48
+
49
+ /**
50
+ * Preserve case of tags being added ie
51
+ * "tag" is different than "Tag"
52
+ * @type {Boolean}
53
+ */
54
+ preserveCase: false,
55
+
56
+ /**
57
+ * Function hook called when a tag is added
58
+ * @param {Event} event Event triggered when tag was added
59
+ * @param {String} tag The tag added
60
+ */
61
+ onTagAdd: () => {},
62
+
63
+ /**
64
+ * Function hook called when a tag is removed
65
+ * @param {Event} event Event triggered when tag was removed
66
+ * @param {String} tag The tag removed
67
+ */
68
+ onTagRemove: () => {}
69
+ };
70
+
71
+ //////////////////////
72
+ // Helper functions //
73
+ //////////////////////
74
+ function _clamp(val, min, max) {
75
+ return Math.min(Math.max(val, min), max);
76
+ }
77
+
78
+ /**
79
+ * Taggle ES6 Class - Modern tagging library
80
+ */
81
+ class Taggle {
82
+ /**
83
+ * Constructor
84
+ * @param {Mixed} el ID of an element or the actual element
85
+ * @param {Object} options
86
+ */
87
+ constructor(el, options) {
88
+ this.settings = Object.assign({}, DEFAULTS, options);
89
+ this.measurements = {
90
+ container: {
91
+ rect: null,
92
+ style: null,
93
+ padding: null
94
+ }
95
+ };
96
+ this.container = el;
97
+ this.tag = {
98
+ values: [],
99
+ elements: []
100
+ };
101
+ this.inputContainer = options.inputContainer;
102
+ this.input = document.createElement('input');
103
+ this.sizer = document.createElement('div');
104
+ this.pasting = false;
105
+ this.placeholder = null;
106
+
107
+ if (this.settings.placeholder) {
108
+ this.placeholder = document.createElement('span');
109
+ }
110
+
111
+ this._backspacePressed = false;
112
+ this._inputPosition = 0;
113
+ this._setMeasurements();
114
+ this._setupTextarea();
115
+ this._attachEvents();
116
+ }
117
+
118
+ /**
119
+ * Gets all the layout measurements up front
120
+ */
121
+ _setMeasurements() {
122
+ this.measurements.container.rect = this.container.getBoundingClientRect();
123
+ const style = window.getComputedStyle(this.container);
124
+ this.measurements.container.style = style;
125
+
126
+ const lpad = parseInt(style.paddingLeft, 10);
127
+ const rpad = parseInt(style.paddingRight, 10);
128
+ const lborder = parseInt(style.borderLeftWidth, 10);
129
+ const rborder = parseInt(style.borderRightWidth, 10);
130
+
131
+ this.measurements.container.padding = lpad + rpad + lborder + rborder;
132
+ }
133
+
134
+ /**
135
+ * Setup the div container for tags to be entered
136
+ */
137
+ _setupTextarea() {
138
+ this.input.type = 'text';
139
+ // Make sure no left/right padding messes with the input sizing
140
+ this.input.style.paddingLeft = 0;
141
+ this.input.style.paddingRight = 0;
142
+ this.input.className = 'taggle_input';
143
+ this.input.tabIndex = 1;
144
+ this.sizer.className = 'taggle_sizer';
145
+
146
+ [...this.container.children].filter(child => child.tagName === 'TAG-OPTION').forEach(tagOption => {
147
+ this.tag.values.push(tagOption.value);
148
+ this.tag.elements.push(tagOption);
149
+ this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
150
+ })
151
+
152
+
153
+ if (this.placeholder) {
154
+ this._hidePlaceholder();
155
+ this.placeholder.classList.add('taggle_placeholder');
156
+ this.container.appendChild(this.placeholder);
157
+ this.placeholder.textContent = this.settings.placeholder;
158
+
159
+ if (!this.tag.values.length) {
160
+ this._showPlaceholder();
161
+ }
162
+ }
163
+
164
+
165
+ const div = document.createElement('div');
166
+ div.appendChild(this.input);
167
+ div.appendChild(this.sizer);
168
+ this.inputContainer.appendChild(div);
169
+ const fontSize = window.getComputedStyle(this.input).fontSize;
170
+ this.sizer.style.fontSize = fontSize;
171
+ }
172
+
173
+ /**
174
+ * Attaches neccessary events
175
+ */
176
+ _attachEvents() {
177
+ if (this._eventsAttached) {
178
+ return false;
179
+ }
180
+ this._eventsAttached = true;
181
+
182
+ this._handleContainerClick = () => this.input.focus();
183
+ this.container.addEventListener('click', this._handleContainerClick);
184
+
185
+ this._handleFocus = this._setFocusStateForContainer.bind(this);
186
+ this._handleBlur = this._blurEvent.bind(this);
187
+ this._handleKeydown = this._keydownEvents.bind(this);
188
+ this._handleKeyup = this._keyupEvents.bind(this);
189
+
190
+ this.input.addEventListener('focus', this._handleFocus);
191
+ this.input.addEventListener('blur', this._handleBlur);
192
+ this.input.addEventListener('keydown', this._handleKeydown);
193
+ this.input.addEventListener('keyup', this._handleKeyup);
194
+
195
+ return true;
196
+ }
197
+
198
+ _detachEvents() {
199
+ if (!this._eventsAttached) {
200
+ return false;
201
+ }
202
+ this._eventsAttached = false;
203
+
204
+ this.container.removeEventListener('click', this._handleContainerClick);
205
+ this.input.removeEventListener('focus', this._handleFocus);
206
+ this.input.removeEventListener('blur', this._handleBlur);
207
+ this.input.removeEventListener('keydown', this._handleKeydown);
208
+ this.input.removeEventListener('keyup', this._handleKeyup);
209
+
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * Returns whether or not the specified tag text can be added
215
+ * @param {Event} e event causing the potentially added tag
216
+ * @param {String} text tag value
217
+ * @return {Boolean}
218
+ */
219
+ _canAdd(e, text) {
220
+ if (!text) {
221
+ return false;
222
+ }
223
+ const limit = this.settings.maxTags;
224
+ if (limit !== null && limit <= this.getTagValues().length) {
225
+ return false;
226
+ }
227
+
228
+ // Check for duplicates
229
+ return this.tag.values.indexOf(text) === -1;
230
+ }
231
+
232
+ /**
233
+ * Appends tag with its corresponding input to the list
234
+ * @param {Event} e
235
+ * @param {String} text
236
+ * @param {Number} index
237
+ */
238
+ _add(e, text, index) {
239
+ let values = text || '';
240
+ const delimiter = ',';
241
+
242
+ if (typeof text !== 'string') {
243
+ values = this.input.value;
244
+
245
+ if (this.settings.trimTags) {
246
+ if (values[0] === delimiter) {
247
+ values = values.replace(delimiter, '');
248
+ }
249
+ values = values.trim();
250
+ }
251
+ }
252
+
253
+ values.split(delimiter).map(val => {
254
+ if (this.settings.trimTags) {
255
+ val = val.trim();
256
+ }
257
+ return this._formatTag(val);
258
+ }).forEach(val => {
259
+ if (!this._canAdd(e, val)) {
260
+ return;
261
+ }
262
+
263
+ const currentTagLength = this.tag.values.length;
264
+ const tagIndex = _clamp(index || currentTagLength, 0, currentTagLength);
265
+ const tagOption = this._createTag(val, tagIndex);
266
+ this.container.append(tagOption);
267
+
268
+ val = this.tag.values[tagIndex];
269
+
270
+ this.settings.onTagAdd(e, val);
271
+
272
+ this.input.value = '';
273
+ this._setMeasurements();
274
+ this._setInputWidth();
275
+ this._setFocusStateForContainer();
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Removes last tag if it has already been probed
281
+ * @param {Event} e
282
+ */
283
+ _checkPrevOrNextTag(e) {
284
+ const taggles = this.container.querySelectorAll('tag-option');
285
+ const prevTagIndex = _clamp(this._inputPosition - 1, 0, taggles.length - 1);
286
+ const nextTagIndex = _clamp(this._inputPosition, 0, taggles.length - 1);
287
+ let index = prevTagIndex;
288
+
289
+ if (e.keyCode === DELETE) {
290
+ index = nextTagIndex;
291
+ }
292
+
293
+ const targetTaggle = taggles[index];
294
+ const hotClass = 'taggle_hot';
295
+ const isDeleteOrBackspace = [BACKSPACE, DELETE].includes(e.keyCode);
296
+
297
+ // prevent holding backspace from deleting all tags
298
+ if (this.input.value === '' && isDeleteOrBackspace && !this._backspacePressed) {
299
+ if (targetTaggle.classList.contains(hotClass)) {
300
+ this._backspacePressed = true;
301
+ this._remove(targetTaggle, e);
302
+ this._setMeasurements();
303
+ this._setInputWidth();
304
+ this._setFocusStateForContainer();
305
+ }
306
+ else {
307
+ targetTaggle.classList.add(hotClass);
308
+ }
309
+ }
310
+ else if (targetTaggle.classList.contains(hotClass)) {
311
+ targetTaggle.classList.remove(hotClass);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Setter for the hidden input.
317
+ * @param {Number} width
318
+ */
319
+ _setInputWidth() {
320
+ const width = this.sizer.getBoundingClientRect().width;
321
+ const max = this.measurements.container.rect.width - this.measurements.container.padding;
322
+ const size = parseInt(this.sizer.style.fontSize, 10);
323
+
324
+ // 1.5 just seems to be a good multiplier here
325
+ const newWidth = Math.round(_clamp(width + (size * 1.5), 10, max));
326
+
327
+ this.input.style.width = `${newWidth}px`;
328
+ }
329
+
330
+ /**
331
+ * Handles focus state of div container.
332
+ */
333
+ _setFocusStateForContainer() {
334
+ this._setMeasurements();
335
+ this._setInputWidth();
336
+
337
+ if (!this.container.classList.contains(this.settings.containerFocusClass)) {
338
+ this.container.classList.add(this.settings.containerFocusClass);
339
+ }
340
+
341
+ this._hidePlaceholder();
342
+ }
343
+
344
+ /**
345
+ * Runs all the events that need to happen on a blur
346
+ * @param {Event} e
347
+ */
348
+ _blurEvent(e) {
349
+ if (this.container.classList.contains(this.settings.containerFocusClass)) {
350
+ this.container.classList.remove(this.settings.containerFocusClass);
351
+ }
352
+
353
+ if (!this.tag.values.length && !this.input.value) {
354
+ this._showPlaceholder();
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Runs all the events that need to run on keydown
360
+ * @param {Event} e
361
+ */
362
+ _keydownEvents(e) {
363
+ const key = e.keyCode;
364
+ this.pasting = false;
365
+
366
+ this._setInputWidth();
367
+
368
+ if (key === 86 && e.metaKey) {
369
+ this.pasting = true;
370
+ }
371
+
372
+ if (this.settings.submitKeys.includes(key) && this.input.value !== '') {
373
+ this._confirmValidTagEvent(e);
374
+ return;
375
+ }
376
+
377
+ if (this.tag.values.length) {
378
+ this._checkPrevOrNextTag(e);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Runs all the events that need to run on keyup
384
+ * @param {Event} e
385
+ */
386
+ _keyupEvents(e) {
387
+ this._backspacePressed = false;
388
+
389
+ this.sizer.textContent = this.input.value;
390
+
391
+ // If we break to a new line because the text is too long
392
+ // and decide to delete everything, we should resize the input
393
+ // so it falls back inline
394
+ if (!this.input.value) {
395
+ this._setInputWidth();
396
+ }
397
+
398
+ if (this.pasting && this.input.value !== '') {
399
+ this._add(e);
400
+ this.pasting = false;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Confirms the inputted value to be converted to a tag
406
+ * @param {Event} e
407
+ */
408
+ _confirmValidTagEvent(e) {
409
+ // prevents from jumping out of textarea
410
+ e.preventDefault();
411
+
412
+ this._add(e, null, this._inputPosition);
413
+ }
414
+
415
+ _createTag(text, index) {
416
+ const tagOption = document.createElement('tag-option');
417
+
418
+ text = this._formatTag(text);
419
+ tagOption.textContent = text;
420
+ tagOption.setAttribute('value', text);
421
+
422
+ this.tag.values.splice(index, 0, text);
423
+ this.tag.elements.splice(index, 0, tagOption);
424
+ this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
425
+
426
+ return tagOption;
427
+ }
428
+
429
+ _showPlaceholder() {
430
+ if (this.placeholder) {
431
+ this.placeholder.style.opacity = 1;
432
+ this.placeholder.setAttribute('aria-hidden', 'false');
433
+ }
434
+ }
435
+
436
+ _hidePlaceholder() {
437
+ if (this.placeholder) {
438
+ this.placeholder.style.opacity = 0;
439
+ this.placeholder.setAttribute('aria-hidden', 'true');
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Removes tag from the tags collection
445
+ * @param {HTMLElement} tagOption List item to remove
446
+ * @param {Event} e
447
+ */
448
+ _remove(tagOption, e) {
449
+ const index = this.tag.elements.indexOf(tagOption);
450
+ if (index === -1) return;
451
+
452
+ const text = this.tag.values[index];
453
+
454
+ tagOption.remove();
455
+ this.tag.elements.splice(index, 1);
456
+ this.tag.values.splice(index, 1);
457
+ this.settings.onTagRemove(e, text);
458
+
459
+ if (index < this._inputPosition) {
460
+ this._inputPosition = _clamp(this._inputPosition - 1, 0, this.tag.values.length);
461
+ }
462
+
463
+ this._setFocusStateForContainer();
464
+ }
465
+
466
+ /**
467
+ * Format the text for a tag
468
+ * @param {String} text Tag text
469
+ * @return {String}
470
+ */
471
+ _formatTag(text) {
472
+ return this.settings.preserveCase ? text : text.toLowerCase();
473
+ }
474
+
475
+ // @todo
476
+ // @deprecated use getTags().values
477
+ getTagValues() {
478
+ return [...this.tag.values];
479
+ }
480
+
481
+ getInput() {
482
+ return this.input;
483
+ }
484
+
485
+ add(text, index) {
486
+ const isArr = Array.isArray(text);
487
+
488
+ if (isArr) {
489
+ text.forEach((tag, i) => {
490
+ if (typeof tag === 'string') {
491
+ this._add(null, tag, index ? index + i : index);
492
+ }
493
+ });
494
+ }
495
+ else {
496
+ this._add(null, text, index);
497
+ }
498
+
499
+ return this;
500
+ }
501
+
502
+ remove(text) {
503
+ const index = this.tag.values.indexOf(text);
504
+ if (index > -1) {
505
+ this._remove(this.tag.elements[index]);
506
+ }
507
+ return this;
508
+ }
509
+
510
+ removeAll() {
511
+ [...this.tag.elements].forEach(element => this._remove(element));
512
+ this._showPlaceholder();
513
+ return this;
514
+ }
515
+
516
+ _setDisabledState(disabled) {
517
+ const elements = [
518
+ ...this.container.querySelectorAll('button'),
519
+ ...this.container.querySelectorAll('input')
520
+ ];
521
+
522
+ elements.forEach((el) => {
523
+ if (disabled) {
524
+ el.setAttribute('disabled', '');
525
+ } else {
526
+ el.removeAttribute('disabled');
527
+ }
528
+ });
529
+
530
+ return this;
531
+ }
532
+
533
+ enable() {
534
+ return this._setDisabledState(false);
535
+ }
536
+
537
+ disable() {
538
+ return this._setDisabledState(true);
539
+ }
540
+
541
+ destroy() {
542
+ this._detachEvents();
543
+ }
544
+ }
545
+
546
+ export default Taggle;