├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── README.md
├── demo.html
├── package.json
├── screenshot.png
├── tagger.css
├── tagger.d.ts
└── tagger.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.html
3 | *.svg
4 | *.png
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.6.2
2 | * fix removing tags containing multiple consecutive spaces [#47](https://github.com/jcubic/tagger/pull/47). Thanks to [nuclear06](https://github.com/nuclear06)
3 |
4 | ## 0.6.1
5 | * fix triggering change event for ReactJS
6 |
7 | ## 0.6.0
8 | * add native change event for the original input element on tag change [#18](https://github.com/jcubic/tagger/issues/18)
9 |
10 | ## 0.5.0
11 | * fix initialization [#23](https://github.com/jcubic/tagger/issues/23). Thanks to [James Lucas](https://github.com/lucasnetau)
12 | * add placeholder option. Thanks to [James Lucas](https://github.com/lucasnetau)
13 |
14 | ## 0.4.5
15 | * fix another wrapping issue [#37](https://github.com/jcubic/tagger/issues/37)
16 |
17 | ## 0.4.4
18 | * fix wrapping issues [#30](https://github.com/jcubic/tagger/pull/30) thanks to [James Lucas](https://github.com/lucasnetau)
19 |
20 | ## 0.4.3
21 | * Fix completion on Safari [#7](https://github.com/jcubic/tagger/issues/7)
22 |
23 | ## 0.4.2
24 | * Fix autocomplete [#22](https://github.com/jcubic/tagger/pull/22)
25 |
26 | ## 0.4.1
27 | * fix typescript definition for completion
28 |
29 | ## 0.4.0
30 | * [Breaking] value in input no longer have space after comma
31 | * fix updating input when deleting tag using backspace
32 | * add option `add_on_blur`
33 | * add option `tag_limit`
34 |
35 | ## 0.3.1
36 | * fix npm package
37 |
38 | ## 0.3.0
39 | * add wrap option
40 | * fix remove_tag API
41 | * make settings optional
42 | * add typescript types
43 |
44 | ## 0.2.3
45 | * fix ambiguous tags
46 |
47 | ## 0.2.2
48 | * reject empty tags
49 |
50 | ## 0.2.1
51 | * Fix remove_tag when links are disabled
52 |
53 | ## 0.2.0
54 | * link option
55 | * working completion
56 | * allow to use querySelectorAll etc.
57 |
58 | ## 0.1.3
59 | * fix inialization in UMD
60 |
61 | ## 0.1.2
62 | * fix bug in adding tags
63 |
64 | ## 0.1.1
65 | * fix initalization of tags from input
66 |
67 | ## 0.1.0
68 | * initial version
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | _____
3 | |_ _|___ ___ ___ ___ ___
4 | | | | .'| . | . | -_| _|
5 | |_| |__,|_ |_ |___|_|
6 | |___|___| version 0.6.2
7 | ```
8 | # [Tagger: Zero dependency, Vanilla JavaScript Tag Editor](https://github.com/jcubic/tagger)
9 |
10 | [](https://www.npmjs.com/package/@jcubic/tagger)
11 |
12 | 
13 |
14 | Tagger was inspired by StackOverflow tag editor. It supposed to be a part of similar QA website that was never created.
15 |
16 | [Online Demo](https://codepen.io/jcubic/pen/YbYpqO)
17 |
18 | ## Installation
19 |
20 | ```
21 | npm install @jcubic/tagger
22 | ```
23 |
24 | or
25 |
26 | ```
27 | yarn add @jcubic/tagger
28 | ```
29 |
30 | ## Usage
31 |
32 | ```
33 | tagger(document.querySelector('[name="tags"]'), {allow_spaces: false});
34 | ```
35 |
36 | Multiple inputs can be created by passing a NodeList or array of elements (eg. document.querySelectorAll()). If only one element is contained in the list then tagger will return the tagger instance, an array of tagger instances will be returned if the number of elements is greater than 1.
37 |
38 | ## Usage with React
39 |
40 | Tagger can easily be used with ReactJS.
41 |
42 | ```javascript
43 | import { useRef, useState, useEffect } from 'react'
44 | import tagger from '@jcubic/tagger'
45 |
46 | const App = () => {
47 | const [tags, setTags] = useState([]);
48 | const inputRef = useRef(null);
49 |
50 | useEffect(() => {
51 | const taggerOptions = {
52 | allow_spaces: true,
53 | };
54 | tagger(inputRef.current, taggerOptions);
55 | onChange();
56 | }, [inputRef]);
57 |
58 | const onChange = () => {
59 | setTags(tags_array(inputRef.current.value));
60 | };
61 |
62 | return (
63 |
64 |
65 |
66 |
67 | {tags.map((tag, index) => - {tag}
)}
68 |
69 |
70 | )
71 | }
72 |
73 | function tags_array(str) {
74 | return str.split(/\s*,\s*/).filter(Boolean);
75 | }
76 |
77 | export default App
78 | ```
79 |
80 | See demo in action on [CodePen](https://codepen.io/jcubic/pen/YzRdbmp?editors=0010).
81 |
82 | ## API
83 |
84 | ### methods:
85 |
86 | * `add_tag(string): boolean`
87 | * `remove_tag(string): booelan`
88 | * `complete(string): void`
89 |
90 | ### Options:
91 |
92 | * **wrap** (default false) allow tags to wrap onto new lines instead of overflow scroll
93 | * **allow_duplicates** (default false)
94 | * **allow_spaces** (default true)
95 | * **add_on_blur** (default false)
96 | * **completion** `{list: string[] | function(): Promise(string[])|string[], delay: miliseconds, min_length: number}`
97 | * **link** `function(name): string|false` it should return what should be in href attribute or false
98 | * **tag_limit** `number` (default -1) limit number of tags, when set to -1 there are no limits
99 | * **placeholder** `string` (default unset) If set in options or on the initial input, this placeholder value will be shown in the tag entry input
100 | * **filter** `function(name): string` it should return the tag name after applying any filters (eg String.toUpperCase()), empty string to filter out tag and prevent creation.
101 |
102 | **NOTE:** if you're familiar with TypeScript you can check the API by looking at
103 | TypeScript definition file:
104 |
105 | [tagger.d.ts](https://github.com/jcubic/tagger/blob/master/tagger.d.ts)
106 |
107 | ## Press
108 | * JavaScript Weekly
109 | * [Issue #527](https://javascriptweekly.com/issues/527)
110 | * [Issue #652](https://javascriptweekly.com/issues/652)
111 | * [Web Tools Weekly](https://webtoolsweekly.com/archives/issue-396/)
112 | * [Minimal Tagging Input In Pure JavaScript – Tagger](https://www.cssscript.com/tagging-input-tagger/)
113 | * [Frontend Focus #657](https://frontendfoc.us/issues/657)
114 |
115 | ## License
116 |
117 | Copyright (c) 2018-2024 [Jakub T. Jankiewicz](https://jcubic.pl/me)
118 | Released under the MIT license
119 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tagger Example
8 |
9 |
14 |
15 |
16 | Tagger Example
17 |
18 |
19 |
20 |
21 |
22 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jcubic/tagger",
3 | "version": "0.6.2",
4 | "description": "Zero dependency, Vanilla JavaScript Tag Editor",
5 | "typings": "tagger.d.ts",
6 | "main": "tagger.js",
7 | "scripts": {
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/jcubic/tagger.git"
12 | },
13 | "keywords": [
14 | "tag",
15 | "editor",
16 | "inline",
17 | "edit-in-place",
18 | "interfence",
19 | "widget",
20 | "component",
21 | "widget",
22 | "ui"
23 | ],
24 | "author": "Jakub T. Jankiewicz (https://jcubic.pl/me/)",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/jcubic/tagger/issues"
28 | },
29 | "homepage": "https://github.com/jcubic/tagger#readme"
30 | }
31 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcubic/tagger/8c321823abc34ddaa097aacaa905511ef695d0fd/screenshot.png
--------------------------------------------------------------------------------
/tagger.css:
--------------------------------------------------------------------------------
1 | /**@license
2 | * _____
3 | * |_ _|___ ___ ___ ___ ___
4 | * | | | .'| . | . | -_| _|
5 | * |_| |__,|_ |_ |___|_|
6 | * |___|___| version 0.6.2
7 | *
8 | * Tagger - Zero dependency, Vanilla JavaScript Tag Editor
9 | *
10 | * Copyright (c) 2018-2024 Jakub T. Jankiewicz
11 | * Released under the MIT license
12 | */
13 | .tagger {
14 | border: 1px solid #909497;
15 | }
16 | .tagger input[type="hidden"] {
17 | /* fix for bootstrap */
18 | display: none;
19 | }
20 | .tagger > ul {
21 | display: flex;
22 | width: 100%;
23 | align-items: center;
24 | padding: 4px 5px 0;
25 | justify-content: space-between;
26 | box-sizing: border-box;
27 | height: auto;
28 | flex: 0 0 auto;
29 | overflow-y: auto;
30 | margin: 0;
31 | list-style: none;
32 | }
33 | .tagger > ul > li {
34 | padding-bottom: 0.4rem;
35 | margin: 0.4rem 5px 4px;
36 | }
37 | .tagger > ul > li:not(.tagger-new) a,
38 | .tagger > ul > li:not(.tagger-new) a:visited {
39 | text-decoration: none;
40 | color: black;
41 | }
42 | .tagger > ul > li:not(.tagger-new) > :first-child {
43 | padding: 4px 4px 4px 8px;
44 | background: #B1C3D7;
45 | border: 1px solid #4181ed;
46 | border-radius: 3px;
47 | }
48 | .tagger > ul > li:not(.tagger-new) > span,
49 | .tagger > ul > li:not(.tagger-new) > a > span {
50 | white-space: nowrap;
51 | }
52 | .tagger li a.close {
53 | padding: 4px;
54 | margin-left: 4px;
55 | /* for bootstrap */
56 | float: none;
57 | filter: alpha(opacity=100);
58 | opacity: 1;
59 | font-size: 16px;
60 | line-height: 16px;
61 | }
62 | .tagger li a.close:hover {
63 | color: white;
64 | }
65 | .tagger .tagger-new input {
66 | border: none;
67 | outline: none;
68 | box-shadow: none;
69 | width: 100%;
70 | padding-left: 0;
71 | box-sizing: border-box;
72 | background: transparent;
73 | }
74 | .tagger .tagger-new {
75 | flex-grow: 1;
76 | position: relative;
77 | min-width: 40px;
78 | width: 1px;
79 | }
80 | .tagger.wrap > ul {
81 | flex-wrap: wrap;
82 | justify-content: start;
83 | }
84 |
--------------------------------------------------------------------------------
/tagger.d.ts:
--------------------------------------------------------------------------------
1 | /**@license
2 | * _____
3 | * |_ _|___ ___ ___ ___ ___
4 | * | | | .'| . | . | -_| _|
5 | * |_| |__,|_ |_ |___|_|
6 | * |___|___| version 0.6.2
7 | *
8 | * Tagger - Zero dependency, Vanilla JavaScript Tag Editor
9 | *
10 | * Copyright (c) 2018-2024 Jakub T. Jankiewicz
11 | * Released under the MIT license
12 | */
13 | declare namespace Tagger {
14 | type TypeOrPromise = T | PromiseLike;
15 | type completion_function = () => TypeOrPromise;
16 | type completion_list = string[] | completion_function;
17 | interface completion {
18 | list: completion_list;
19 | delay: number;
20 | min_length: number;
21 | }
22 | type link = (name: string) => (string | false);
23 | type filter = (name: string) => (string);
24 | }
25 |
26 | interface tagger_options {
27 | wrap?: boolean;
28 | allow_duplicates?: boolean;
29 | allow_spaces?: boolean;
30 | add_on_blur?: boolean;
31 | tag_limit?: number;
32 | completion?: Tagger.completion;
33 | link?: Tagger.link;
34 | placeholder?: string;
35 | filter?: Tagger.filter;
36 | }
37 |
38 | interface tagger_instance {
39 | add_tag(name: string): boolean;
40 | remove_tag(name: string): boolean;
41 | complete(name: string): void;
42 | }
43 |
44 | export default function tagger(element: HTMLElement, option?: tagger_options): tagger_instance;
45 |
--------------------------------------------------------------------------------
/tagger.js:
--------------------------------------------------------------------------------
1 | /**@license
2 | * _____
3 | * |_ _|___ ___ ___ ___ ___
4 | * | | | .'| . | . | -_| _|
5 | * |_| |__,|_ |_ |___|_|
6 | * |___|___| version 0.6.2
7 | *
8 | * Tagger - Zero dependency, Vanilla JavaScript Tag Editor
9 | *
10 | * Copyright (c) 2018-2024 Jakub T. Jankiewicz
11 | * Released under the MIT license
12 | */
13 | /* global define, module, global */
14 | (function(root, factory, undefined) {
15 | if (typeof define === 'function' && define.amd) {
16 | define([], factory);
17 | } else if (typeof module === 'object' && module.exports) {
18 | module.exports = factory();
19 | } else {
20 | root.tagger = factory();
21 | }
22 | })(typeof window !== 'undefined' ? window : global, function(undefined) {
23 | // ------------------------------------------------------------------------------------------
24 | var get_text = (function() {
25 | var div = document.createElement('div');
26 | var text = ('innerText' in div) ? 'innerText' : 'textContent';
27 | return function(element) {
28 | return element[text];
29 | };
30 | })();
31 | // ------------------------------------------------------------------------------------------
32 | function tagger(input, options) {
33 | if (input.length === 0) {
34 | return;
35 | } else if (input.length === 1) {
36 | input = Array.from(input).pop();
37 | }
38 | if (input.length) {
39 | return Array.from(input).map(function(input) {
40 | return new tagger(input, options);
41 | });
42 | }
43 | if (!(this instanceof tagger)) {
44 | return new tagger(input, options);
45 | }
46 | var settings = merge({}, tagger.defaults, options);
47 | this.init(input, settings);
48 | }
49 | // ------------------------------------------------------------------------------------------
50 | function merge() {
51 | if (arguments.length < 2) {
52 | return arguments[0];
53 | }
54 | var target = arguments[0];
55 | [].slice.call(arguments).reduce(function(acc, obj) {
56 | if (is_object(obj)) {
57 | Object.keys(obj).forEach(function(key) {
58 | if (is_object(obj[key])) {
59 | if (is_object(acc[key])) {
60 | acc[key] = merge({}, acc[key], obj[key]);
61 | return;
62 | }
63 | }
64 | acc[key] = obj[key];
65 | });
66 | }
67 | return acc;
68 | });
69 | return target;
70 | }
71 | // ------------------------------------------------------------------------------------------
72 | function is_object(arg) {
73 | if (typeof arg !== 'object' || arg === null) {
74 | return false;
75 | }
76 | return Object.prototype.toString.call(arg) === '[object Object]';
77 | }
78 | // ------------------------------------------------------------------------------------------
79 | function create(tag, attrs, children) {
80 | tag = document.createElement(tag);
81 | Object.keys(attrs).forEach(function(name) {
82 | if (name === 'style') {
83 | Object.keys(attrs.style).forEach(function(name) {
84 | tag.style[name] = attrs.style[name];
85 | });
86 | } else {
87 | tag.setAttribute(name, attrs[name]);
88 | }
89 | });
90 | if (children !== undefined) {
91 | children.forEach(function(child) {
92 | var node;
93 | if (typeof child === 'string') {
94 | node = document.createTextNode(child);
95 | } else {
96 | node = create.apply(null, child);
97 | }
98 | tag.appendChild(node);
99 | });
100 | }
101 | return tag;
102 | }
103 | // ------------------------------------------------------------------------------------------
104 | function escape_regex(str) {
105 | var special = /([-\\^$[\]()+{}?*.|])/g;
106 | return str.replace(special, '\\$1');
107 | }
108 | var id = 0;
109 | // ------------------------------------------------------------------------------------------
110 | tagger.defaults = {
111 | allow_duplicates: false,
112 | allow_spaces: true,
113 | completion: {
114 | list: [],
115 | delay: 400,
116 | min_length: 2
117 | },
118 | tag_limit: -1,
119 | add_on_blur: false,
120 | link: function(name) {
121 | return '/tag/' + name;
122 | },
123 | filter: (name) => name,
124 | };
125 | // ------------------------------------------------------------------------------------------
126 | tagger.fn = tagger.prototype = {
127 | init: function(input, settings) {
128 | this._id = ++id;
129 | this._settings = settings || {};
130 | this._ul = document.createElement('ul');
131 | this._input = input;
132 | var wrapper = document.createElement('div');
133 | if (settings.wrap) {
134 | wrapper.className = 'tagger wrap';
135 | } else {
136 | wrapper.className = 'tagger';
137 | }
138 | if (!settings.placeholder && this._input.hasAttribute('placeholder')) {
139 | settings.placeholder = this._input.placeholder;
140 | }
141 | this._input.setAttribute('hidden', 'hidden');
142 | var li = document.createElement('li');
143 | li.className = 'tagger-new';
144 | this._new_input_tag = document.createElement('input');
145 | this.tags_from_input();
146 | if (settings.placeholder) {
147 | this._new_input_tag.setAttribute('placeholder', settings.placeholder);
148 | }
149 | li.appendChild(this._new_input_tag);
150 | this._completion = document.createElement('div');
151 | this._completion.className = 'tagger-completion';
152 | this._ul.appendChild(li);
153 | input.parentNode.replaceChild(wrapper, input);
154 | wrapper.appendChild(input);
155 | wrapper.appendChild(this._ul);
156 | li.appendChild(this._completion);
157 | this._add_events();
158 | this._toggle_completion(false);
159 | if (this._settings.completion.list instanceof Array) {
160 | this._build_completion(this._settings.completion.list);
161 | }
162 | },
163 | _update_input: function () {
164 | // ReactJS overwrite value setting on inputs, this is a workaround
165 | // ref: https://stackoverflow.com/a/46012210/387194
166 | var inputProto = window.HTMLInputElement.prototype;
167 | var nativeInputValueSetter = Object.getOwnPropertyDescriptor(inputProto, 'value').set;
168 | nativeInputValueSetter.call(this._input, this._tags.join(','));
169 | this._input.dispatchEvent(new Event('input', { bubbles: true }));
170 | },
171 | // --------------------------------------------------------------------------------------
172 | _add_events: function() {
173 | var self = this;
174 | this._ul.addEventListener('click', function(event) {
175 | if (event.target.className.match(/close/)) {
176 | self._remove_tag(event.target);
177 | event.preventDefault();
178 | } else if (event.target.tagName === 'UL') { //Focus new input when clicking in the whitespace of the Tagger instance
179 | self._new_input_tag.focus();
180 | }
181 | });
182 | if (this._settings.add_on_blur) {
183 | this._new_input_tag.addEventListener('blur', function(event) {
184 | if (self.add_tag(self._new_input_tag.value.trim())) {
185 | self._new_input_tag.value = '';
186 | }
187 | });
188 | }
189 | // ----------------------------------------------------------------------------------
190 | this._new_input_tag.addEventListener('keydown', function(event) {
191 | if (event.keyCode === 13 || event.keyCode === 188 ||
192 | (event.keyCode === 32 && !self._settings.allow_spaces)) { // enter || comma || space
193 | if (self.add_tag(self._new_input_tag.value.trim())) {
194 | self._new_input_tag.value = '';
195 | }
196 | event.preventDefault();
197 | } else if (event.keyCode === 8 && !self._new_input_tag.value) { // backspace
198 | if (self._tags.length > 0) {
199 | var li = self._ul.querySelector('li:nth-last-child(2)');
200 | self._ul.removeChild(li);
201 | self._tags.pop();
202 | self._update_input();
203 | }
204 | event.preventDefault();
205 | } else if (event.keyCode === 32 && (event.ctrlKey || event.metaKey)) {
206 | if (typeof self._settings.completion.list === 'function') {
207 | self.complete(self._new_input_tag.value);
208 | }
209 | self._toggle_completion(true);
210 | event.preventDefault();
211 | } else if (self._tag_limit() && event.keyCode !== 9) { // tab
212 | event.preventDefault();
213 | }
214 | });
215 | // ----------------------------------------------------------------------------------
216 | this._new_input_tag.addEventListener('input', function(event) {
217 | var value = self._new_input_tag.value;
218 | if (self._tag_selected(value)) {
219 | if (self.add_tag(value)) {
220 | self._toggle_completion(false);
221 | self._new_input_tag.value = '';
222 | }
223 | } else {
224 | var min = self._settings.completion.min_length;
225 | if (typeof self._settings.completion.list === 'function' && value.length >= min) {
226 | self.complete(value);
227 | }
228 | self._toggle_completion(value.length >= min);
229 | }
230 | });
231 | // ----------------------------------------------------------------------------------
232 | this._completion.addEventListener('click', function(event) {
233 | if (event.target.tagName.toLowerCase() === 'a') {
234 | self.add_tag(get_text(event.target));
235 | self._new_input_tag.value = '';
236 | self._completion.innerHTML = '';
237 | }
238 | });
239 | },
240 | // --------------------------------------------------------------------------------------
241 | _tag_selected: function(tag) {
242 | if (this._last_completion) {
243 | if (this._last_completion.includes(tag)) {
244 | var re = new RegExp('^' + escape_regex(tag));
245 | return this._last_completion.filter(function(test_tag) {
246 | return re.test(test_tag);
247 | }).length === 1;
248 | }
249 | }
250 | return false;
251 | },
252 | // --------------------------------------------------------------------------------------
253 | _toggle_completion: function(toggle) {
254 | if (toggle) {
255 | this._new_input_tag.setAttribute('list', 'tagger-completion-' + this._id);
256 | } else {
257 | this._new_input_tag.setAttribute('list', 'tagger-completion-disabled-' + this._id);
258 | }
259 | },
260 | // --------------------------------------------------------------------------------------
261 | _build_completion: function(list) {
262 | this._completion.innerHTML = '';
263 | this._last_completion = list;
264 | if (list.length) {
265 | var id = 'tagger-completion-' + this._id;
266 | if (!this._settings.allow_duplicates) {
267 | list = list.filter(x => !this._tags.includes(x));
268 | }
269 | var datalist = create('datalist', {id: id}, list.map(function(tag) {
270 | return ['option', {}, [tag]];
271 | }));
272 | this._completion.appendChild(datalist);
273 | }
274 | },
275 | // --------------------------------------------------------------------------------------
276 | complete: function(value) {
277 | if (this._settings.completion) {
278 | var list = this._settings.completion.list;
279 | if (typeof list === 'function') {
280 | var ret = list(value);
281 | if (ret && typeof ret.then === 'function') {
282 | ret.then(this._build_completion.bind(this));
283 | } else if (ret instanceof Array) {
284 | this._build_completion(ret);
285 | }
286 | } else {
287 | this._build_completion(list);
288 | }
289 | }
290 | },
291 | // --------------------------------------------------------------------------------------
292 | tags_from_input: function() {
293 | this._tags = this._input.value.split(/\s*,\s*/).filter(Boolean);
294 | this._tags.forEach(this._new_tag.bind(this));
295 | },
296 | // --------------------------------------------------------------------------------------
297 | _new_tag: function(name) {
298 | var close = ['a', {href: '#', 'class': 'close'}, ['\u00D7']];
299 | var label = ['span', {'class': 'label'}, [name]];
300 | var href = this._settings.link(name);
301 | var li;
302 | if (href === false) {
303 | li = create('li', {}, [['span', {}, [label, close]]]);
304 | } else {
305 | var a_atts = {href: href, target: '_black'};
306 | li = create('li', {}, [['a', a_atts, [label, close]]]);
307 | }
308 | this._ul.insertBefore(li, this._new_input_tag.parentNode);
309 | },
310 | // --------------------------------------------------------------------------------------
311 | _tag_limit: function() {
312 | return this._settings.tag_limit > 0 && this._tags.length >= this._settings.tag_limit;
313 | },
314 | // --------------------------------------------------------------------------------------
315 | add_tag: function(name) {
316 | if (this._tag_limit()) {
317 | return false;
318 | }
319 | name = this._settings.filter(name);
320 | if (this.is_empty(name)) {
321 | return false;
322 | }
323 | if (!this._settings.allow_duplicates && this._tags.indexOf(name) !== -1) {
324 | return false;
325 | }
326 | this._new_tag(name);
327 | this._tags.push(name);
328 | this._update_input();
329 | return true;
330 | },
331 | // --------------------------------------------------------------------------------------
332 | is_empty: function(value) {
333 | switch(value) {
334 | case '':
335 | case '""':
336 | case "''":
337 | case '``':
338 | case undefined:
339 | case null:
340 | return true;
341 | default:
342 | return false;
343 | }
344 | },
345 | // --------------------------------------------------------------------------------------
346 | remove_tag: function(name, remove_dom = true) {
347 | this._tags = this._tags.filter(function(tag) {
348 | return name !== tag;
349 | });
350 | this._update_input();
351 | if (remove_dom) {
352 | var tags = Array.from(this._ul.querySelectorAll('.label'));
353 | var re = new RegExp('^\s*' + escape_regex(name) + '\s*$');
354 | var span = tags.find(function(node) {
355 | return node.innerText.match(re);
356 | });
357 | if (!span) {
358 | return false;
359 | }
360 | var li = span.closest('li');
361 | this._ul.removeChild(li);
362 | return true;
363 | }
364 | },
365 | // --------------------------------------------------------------------------------------
366 | _remove_tag: function(close) {
367 | var li = close.closest('li');
368 | var name = li.querySelector('.label').textContent;
369 | this._ul.removeChild(li);
370 | this.remove_tag(name, false);
371 | }
372 | };
373 | // ------------------------------------------------------------------------------------------
374 | return tagger;
375 | });
376 |
--------------------------------------------------------------------------------