├── .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 | [![npm](https://img.shields.io/badge/npm-0.6.2-blue.svg)](https://www.npmjs.com/package/@jcubic/tagger) 11 | 12 | ![Tag Editor widget in JavaScript](https://raw.githubusercontent.com/jcubic/tagger/master/screenshot.png) 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 | 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 | --------------------------------------------------------------------------------