├── .gitignore ├── bower.json ├── Gruntfile.js ├── demo └── index.html ├── package.json ├── input_style.css ├── tokenizer.min.js ├── README.md └── tokenizer.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "input-tokenizer", 3 | "version": "1.3.2", 4 | "homepage": "https://www.donmccurdy.com/input-tokenizer/", 5 | "authors": [ 6 | "Don McCurdy " 7 | ], 8 | "description": "jQuery plugin that allows a user to type keywords, which will be broken up into tokens/tags and displayed, similarly to tagging a post on Tumblr or Stack-Overflow.", 9 | "main": ["tokenizer.js", "input_style.css"], 10 | "keywords": [ 11 | "autocomplete", 12 | "tag", 13 | "ui", 14 | "elements", 15 | "autosuggest", 16 | "text", 17 | "input", 18 | "jquery-plugin", 19 | "ecosystem:jquery" 20 | ], 21 | "license": "MIT", 22 | "dependencies": { 23 | "jquery": "^1.8.0" 24 | }, 25 | "ignore": [ 26 | "demo" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | 6 | /* JS MINIFICATION 7 | **********************************/ 8 | uglify: { 9 | main: { 10 | options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, 11 | files: { 'tokenizer.min.js': 'tokenizer.js' } 12 | } 13 | }, 14 | 15 | /* SEMVER HELPER 16 | **********************************/ 17 | bump: { 18 | options: { 19 | pushTo: 'origin', 20 | files: ['package.json', 'bower.json'], 21 | commitFiles: ['package.json', 'bower.json', 'tokenizer.min.js'] 22 | } 23 | } 24 | 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-contrib-uglify'); 28 | grunt.loadNpmTasks('grunt-bump'); 29 | 30 | grunt.registerTask('build', ['uglify']); 31 | }; 32 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Input Tokenizer | Demo 5 | 20 | 21 | 22 | 23 | 24 |
25 |

Input Tokenizer | Demo

26 | 27 |
28 | 29 | 30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "input-tokenizer", 3 | "version": "1.3.2", 4 | "description": "jQuery plugin that allows a user to type keywords, which will be broken up into tokens/tags and displayed, similarly to tagging a post on Tumblr or Stack-Overflow.", 5 | "main": "tokenizer.js", 6 | "style": "input_style.css", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/donmccurdy/input-tokenizer.git" 10 | }, 11 | "keywords": [ 12 | "autocomplete", 13 | "tag", 14 | "ui", 15 | "elements", 16 | "autosuggest", 17 | "text", 18 | "input", 19 | "jquery-plugin", 20 | "ecosystem:jquery" 21 | ], 22 | "author": "Don McCurdy ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/donmccurdy/input-tokenizer/issues" 26 | }, 27 | "homepage": "https://www.donmccurdy.com/input-tokenizer/", 28 | "dependencies": { 29 | "jquery": "^3.3.1" 30 | }, 31 | "devDependencies": { 32 | "grunt": "^0.4.5", 33 | "grunt-bump": "^0.3.0", 34 | "grunt-contrib-uglify": "^0.9.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /input_style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Example styling for tokenizer plugin. 3 | * Prefix may be overriden with 'namespace' option. 4 | * Don McCurdy, 2013. 5 | */ 6 | 7 | .tknz-wrapper { /* The apparent input */ 8 | width: 350px; 9 | height: 250px; 10 | margin: 50px auto; 11 | padding: 10px; 12 | overflow: auto; 13 | 14 | color: #fefefe; 15 | background: #fefefe; 16 | font-family: "Courier", Times, sans-serif; 17 | 18 | border: solid 2px #DFDFDF; 19 | border-radius:10px; 20 | } 21 | 22 | .tknz-focus{ /* Apparent input in active state */ 23 | box-shadow: 0px 0px 6px #5c7db7; 24 | border: solid 2px transparent; 25 | } 26 | 27 | .tknz-wrapper-label { /* Label for the field */ 28 | float: left; 29 | color: #000; 30 | margin: 5px 5px 0 2px; 31 | } 32 | 33 | .tknz-input { /* The actual input */ 34 | font-family: inherit; 35 | font-size: inherit; 36 | font-weight: inherit; 37 | width: 112px; 38 | margin: 2px; 39 | border: none; 40 | outline: none; 41 | background: transparent; 42 | } 43 | 44 | .tknz-input-wrapper { 45 | float: left; 46 | position: relative; 47 | top: 4px; 48 | } 49 | 50 | .tknz-token { /* An individual token */ 51 | background: #5c7db7; 52 | padding: 2px 2px 2px 5px; 53 | margin: 2px; 54 | float: left; 55 | border-radius: 2px; 56 | max-width: 340px; 57 | cursor: pointer; 58 | } 59 | 60 | .tknz-token-label { /* Token's label */ 61 | word-break: break-word; 62 | } 63 | 64 | .tknz-token-x { /* Delete button for token */ 65 | color: rgba(255,255,255, 0.5); 66 | padding-left: 7px; 67 | position: relative; 68 | top: 1px; 69 | } 70 | .tknz-token-x:hover { 71 | text-shadow: 0px 0px 3px #eee; 72 | color: #fff; 73 | } 74 | 75 | .tknz-suggest { /* Autosuggest modal */ 76 | position: absolute; 77 | top: 27px; 78 | left: 5px; 79 | background-color: #fff; 80 | box-shadow: 1px 1px 3px rgba(120,120,120, 0.3); 81 | } 82 | 83 | .tknz-suggest ul { 84 | list-style: none; 85 | margin: 0px; 86 | padding: 0px; 87 | text-align: left; 88 | } 89 | 90 | .tknz-suggest-li { 91 | color: #000; 92 | padding: 0px 5px; 93 | } 94 | 95 | .tknz-suggest-li:nth-child(2n) { 96 | background-color: #f0f0f0; 97 | } 98 | 99 | .tknz-suggest-li.tknz-sel { 100 | color: #fff; 101 | background-color: #5c7db7; 102 | } 103 | 104 | /** MEDIA **/ 105 | 106 | @media screen and (max-width: 480px) { 107 | .tknz-input { 108 | width: 90%; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tokenizer.min.js: -------------------------------------------------------------------------------- 1 | /*! input-tokenizer 2018-03-04 */ 2 | !function(a){"object"==typeof module&&"object"==typeof module.exports?module.exports=a(require("jquery"),window):a(jQuery,window)}(function(a,b,c){var d="tokenizer",e=function(d,e){var f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y=a({}),z=a.extend({xContent:"×",namespace:"tknz",label:"Tags:",placeholder:"",separators:[","," ","."],callback:null,source:null,allowUnknownTags:!0,numToSuggest:5,onclick:null,allowDuplicates:!1},e);return j=function(){f=d,k(),l()},k=function(){var b=z.namespace;g=a('
'),z.placeholder&&f.attr("placeholder",z.placeholder),i=a('
    '),h=f.addClass(b+"-input").wrap('').parent().wrap('
    ').parent().prepend(g).prepend(''+z.label+"").find("."+b+"-input-wrapper").append(i).end()},l=function(){var c=z.namespace;h.on("focus","input",function(){h.addClass(c+"-focus")}).on("blur","input",function(){h.removeClass(c+"-focus"),y.delay(200).queue().push(function(){z.source?t([],""):v(f.val()),a.dequeue(this)})}).on("keydown","input",function(a){if(a=a||b.event,a.which=a.which||a.keyCode||a.charCode,8===a.which&&!f.val())return n(),void s();var d=c+"-sel",e=i.find("."+d);38===a.which?(a.preventDefault(),e.length?e.removeClass(d).prev("li").add(e.siblings().last()).eq(0).addClass(d):i.find("li").last().addClass(d)):40===a.which&&(a.preventDefault(),e.length?e.removeClass(d).next("li").addClass(d):i.find("li").first().addClass(d))}).on("keypress","input",function(a){(z.separators.indexOf(String.fromCharCode(a.which))>-1||13===a.which)&&(a.preventDefault(),v(f.val()))}).on("keyup","input",function(c){c=c||b.event,c.which=c.which||c.keyCode||c.charCode,38!==c.which&&40!==c.which&&(a.isArray(z.source)?t(z.source):z.source&&z.source(f.val(),t))}).on("click",function(){f.focus()}).on("click","."+c+"-token-x",function(b){b.stopPropagation(),a(this).closest("."+c+"-token").remove(),s()}).on("mousedown","."+c+"-suggest-li",function(b){b.preventDefault(),f.val(""),m(a(this).text()),t([]),s()}).on("click","."+c+"-token",function(b){z.onclick&&(b.stopPropagation(),z.onclick(a(this).children("."+c+"-token-label").text()))})},v=function(a){var b=u();a&&(z.allowUnknownTags||b)&&(m(b||a),f.val(""),s()),t([],"")},m=function(a){var b=x(a);if(z.allowDuplicates||b){var c=z.namespace,d=c+"-token",e='
    '+a.trim()+''+z.xContent+"
    ";g.append(e)}return f},n=function(){return g.children().last().detach().data("token")||null},o=function(b){var c=g.children().filter(function(){return a(this).data("token")==b}).detach();return c.length>0?1===c.length?c.data("token"):c.length:null},q=function(){return g.empty(),f},p=function(){var a,b=[],c=g.children();for(a=0;a'+a[d]+"");i.children("ul").html(k.join("")).end()[k.length?"addClass":"removeClass"]("."+e+"-vis")},u=function(){return i.find("."+z.namespace+"-sel").eq(0).text()},w=function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},x=function(a){return-1===p().indexOf(a)},j(d),{push:m,pop:n,remove:o,empty:q,get:p,destroy:r,callback:s}},f={init:function(a){return"INPUT"!==this[0].nodeName?(console.error('Tokenizer requires an tag.'),this):this.data(d,e(this,a))},push:function(a){return this.data(d).push(a)},pop:function(){return this.data(d).pop()},remove:function(a){return this.data(d).remove(a)},empty:function(){return this.data(d).empty()},get:function(){return this.data(d).get()},destroy:function(){return this.data(d).destroy()},callback:function(){return this.data(d).callback()}};return a.fn[d]=function(a){return f[a]?(this.data(d)||console.error('Cannot call "'+a+'" - Tokenizer not initialized.'),f[a].apply(this,Array.prototype.slice.call(arguments,1))):"object"!=typeof a&&a?void console.error("Unknown tokenizer method "+a+"."):f.init.apply(this,arguments)},a.fn[d]}); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tag / Token Input 2 | =============== 3 | 4 | *Features*: Optional autosuggest, custom callbacks, easy setup, custom CSS prefixing. 5 | 6 | *Demo*: [donmccurdy.github.io/input-tokenizer/](https://donmccurdy.github.io/input-tokenizer/) 7 | 8 | *Size*: 1.7kb gzipped, 4.1kb minified. 9 | 10 | *Dependencies*: jQuery 1.8-2.0+. (I haven't tested below 1.8, but it might work...) 11 | 12 | jQuery plugin that stylizes an input and allows a user to type keywords, which will be broken up into tokens/tags and displayed separately. It's what you'd expect to see when tagging a post on Tumblr or Stack-Overflow. Mostly, I just wanted to do this myself, from scratch. 13 | 14 | Screenshot: 15 | 16 | ![A screenshot of the input form.](https://cloud.githubusercontent.com/assets/1848368/7214289/eec96300-e559-11e4-92f2-3859b9f3fc00.png) 17 | 18 | ## Getting Started 19 | 20 | To get started, here are the steps: 21 | 22 | * First, you'll need jQuery and my plugin. jQuery should be included first, 23 | and the path to the plugin will depend on where you put it. 24 | For example, put this between the \\ tags of your HTML: 25 | 26 | ```html 27 | 28 | 29 | ``` 30 | 31 | * Next, copy my sample CSS or tweak it to your needs, then include it as well: 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | * Finally, call the plugin on your input at the end of your \\ tag 38 | contents or once the page has loaded. If you've added the ".myTokenInput" class 39 | to yours, you might do something like this: 40 | 41 | ```html 42 | 45 | ``` 46 | 47 | * You're done! Mess around with the CSS if you want to restyle things a bit. 48 | 49 | ## Methods and Options 50 | 51 | * If you need to do real work with this plugin, you'll probably want to know the 52 | methods and options. Here they are: 53 | 54 | ```javascript 55 | // Initialize with default options 56 | var $input = $(input).tokenizer({}); 57 | 58 | // Initialize with some custom options: 59 | var options = { 60 | /* custom options here */ 61 | } 62 | $input2 = $(input2).tokenizer(options); 63 | 64 | ``` 65 | 66 | Available options: 67 | 68 | ```javascript 69 | { 70 | source: null, // autosuggest options. May be an array or a function. 71 | // If a function is given, it should take two parameters: 72 | // the first will be the input word, the second is a function which should be called 73 | // with a list of terms to suggest. (If you're using Ajax, call this function after your 74 | // response from the server is received, passing an array as the only parameter.) 75 | 76 | allowUnknownTags: true, // if false, prevents user from creating tags not on the autosuggest list 77 | numToSuggest: 5, //number of options to show in autosuggest list. If 0, all results are shown. 78 | 79 | 80 | xContent: '×', // content of the delete button 81 | namespace: 'tknz', // used as class prefix for your CSS-styling pleasure. 82 | label: 'Tags:', // label at top of input 83 | placeholder: '', // placeholder text shown inside the input 84 | 85 | 86 | separators: [',', ' ', '.'], // trigger characters to separate tokens. 87 | // Use [',', '.'] to allow multiple words per tag. 88 | 89 | callback: function ($input) {}, // function to call when the token list changes. 90 | 91 | onclick: function (word) {} // Function to call when a token is clicked. 92 | // Token text is passed as only parameter. 93 | } 94 | ``` 95 | 96 | 97 | Available methods: 98 | 99 | ```javascript 100 | 101 | // 'get' - return list of tokens 102 | var list = $input.tokenizer('get'); // ['unbought','stuffed','dogs'] 103 | 104 | // 'push' - Manually add a token 105 | $input.tokenizer('push', 'YOLO'); // adds 'YOLO' as a token. 106 | 107 | // 'pop' - Get the most recent token 108 | var lastToken = $input.tokenizer('pop'); // returns last token in list. 109 | 110 | // 'remove' - Manually remove a token 111 | $input.tokenizer('remove', 'YOLO'); // removes 'YOLO' from list. 112 | 113 | // 'empty' - Clear the input 114 | $input.tokenizer('empty'); // token list is now empty. 115 | 116 | // 'destroy' - Un-tokenize the input (returns everything to pre-plugin state) 117 | $input.tokenizer('destroy'); // just an everyday input now. 118 | 119 | // 'callback' - Manually trigger the callback function 120 | $input.tokenizer('callback'); // triggers provided callback, if any. 121 | ``` 122 | 123 | ## Other Notes 124 | 125 | If this isn't what you need, there are other great options out there. Try these: 126 | 127 | * http://aehlke.github.com/tag-it/ (recommended) 128 | * http://xoxco.com/projects/code/tagsinput/ 129 | * http://loopj.com/jquery-tokeninput/ 130 | * http://tagedit.webwork-albrecht.de/ 131 | 132 | ## Contributors 133 | 134 | - Don McCurdy 135 | - Adam Skowron 136 | - Bob Frost 137 | 138 | ## Open Source License 139 | 140 | The MIT License (MIT) 141 | Copyright (c) 2013 Don McCurdy 142 | 143 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 144 | 145 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 148 | -------------------------------------------------------------------------------- /tokenizer.js: -------------------------------------------------------------------------------- 1 | // Input Tokenizer 2 | // Author: Don McCurdy 3 | 4 | (function (factory) { 5 | if(typeof module === 'object' && typeof module.exports === 'object') { 6 | module.exports = factory(require('jquery'), window); 7 | } else { 8 | factory(jQuery, window); 9 | } 10 | }(function ($, window, undefined) { 11 | var tokenizer = 'tokenizer'; 12 | 13 | var Tokenizer = function (argElement, argOpts) { 14 | 15 | // PRIVATE VARS 16 | var 17 | input, 18 | list, 19 | wrap, 20 | suggestions, 21 | eventQueue = $({}), 22 | options = $.extend({ 23 | xContent: '×', 24 | namespace: 'tknz', 25 | label: 'Tags:', 26 | placeholder: '', 27 | separators: [',', ' ', '.'], 28 | callback: null, 29 | source: null, 30 | allowUnknownTags: true, 31 | numToSuggest: 5, 32 | onclick: null, 33 | allowDuplicates: false 34 | }, argOpts); 35 | 36 | // PRIVATE METHODS 37 | var init, buildHTML, bindEvents, push, pop, remove, get, empty, 38 | destroy, callback, suggest, getMatch, tryPush, escapeRegExp, 39 | isFirstOccurrence; 40 | 41 | init = function () { 42 | input = argElement; 43 | buildHTML(); 44 | bindEvents(); 45 | }; 46 | buildHTML = function () { 47 | var ns = options.namespace; 48 | list = $('
    '); 49 | if (options.placeholder) { input.attr('placeholder', options.placeholder); } 50 | suggestions = $('
      '); 51 | wrap = input 52 | .addClass(ns+'-input') 53 | .wrap('') 54 | .parent() 55 | .wrap('
      ') 56 | .parent() 57 | .prepend(list) 58 | .prepend(''+options.label+'') 59 | .find('.'+ns+'-input-wrapper').append(suggestions).end(); 60 | }; 61 | bindEvents = function () { 62 | var ns = options.namespace; 63 | wrap 64 | .on('focus', 'input', function () { // On focus, stylize wrapper. 65 | wrap.addClass(ns+'-focus'); 66 | }).on('blur', 'input', function () { // On blur, un-stylize. 67 | wrap.removeClass(ns+'-focus'); 68 | eventQueue.delay(200).queue().push(function () { 69 | // On blur, tag remaining text only if autocomplete is disabled. 70 | if (options.source) { 71 | suggest([], ''); 72 | } else { 73 | tryPush(input.val()); 74 | } 75 | $.dequeue(this); 76 | }); 77 | }).on('keydown', 'input', function (event) { // Backspace handler. 78 | event = event || window.event; 79 | event.which = event.which || event.keyCode || event.charCode; 80 | if (event.which === 8 && !input.val()) { 81 | pop(); 82 | callback(); 83 | return; 84 | } 85 | var 86 | selectClass = ns+'-sel', 87 | selected = suggestions.find('.'+selectClass); 88 | 89 | if (event.which === 38) { // Up 90 | event.preventDefault(); 91 | if (selected.length) { 92 | selected.removeClass(selectClass) 93 | .prev('li').add(selected.siblings().last()).eq(0).addClass(selectClass); 94 | } else { 95 | suggestions.find('li').last().addClass(selectClass); 96 | } 97 | } else if (event.which === 40) { // Down 98 | event.preventDefault(); 99 | if (selected.length) { 100 | selected.removeClass(selectClass) 101 | .next('li').addClass(selectClass); 102 | } else { 103 | suggestions.find('li').first().addClass(selectClass); 104 | } 105 | } 106 | }).on('keypress', 'input', function (event) { // Input listener to create tokens. 107 | if (options.separators.indexOf(String.fromCharCode(event.which)) > -1 || event.which === 13) { 108 | event.preventDefault(); 109 | tryPush(input.val()); 110 | } 111 | }).on('keyup', 'input', function (event) { 112 | event = event || window.event; 113 | event.which = event.which || event.keyCode || event.charCode; 114 | if (event.which === 38 || event.which === 40) { return; } 115 | if ($.isArray(options.source)) { // Autosuggest from list 116 | suggest(options.source); 117 | } else if (options.source) { // Autosuggest from function 118 | options.source(input.val(), suggest); 119 | } 120 | }).on('click', function () { // On click, focus the input. 121 | input.focus(); 122 | }).on('click', '.'+ns+'-token-x', function (event) { 123 | event.stopPropagation(); 124 | $(this).closest('.'+ns+'-token').remove(); 125 | callback(); 126 | }).on('mousedown', '.'+ns+'-suggest-li', function (e) { 127 | e.preventDefault(); // Prevent blur event 128 | input.val(''); 129 | push($(this).text()); 130 | suggest([]); 131 | callback(); 132 | }).on('click', '.'+ns+'-token', function (event) { 133 | if (options.onclick) { 134 | event.stopPropagation(); 135 | options.onclick($(this).children('.'+ns+'-token-label').text()); 136 | } 137 | }); 138 | }; 139 | 140 | tryPush = function (value) { 141 | var match = getMatch(); 142 | if (value && (options.allowUnknownTags || match)) { 143 | push(match || value); 144 | input.val(''); 145 | callback(); 146 | } 147 | suggest([], ''); 148 | }; 149 | push = function (value) { 150 | var firstOccurrence = isFirstOccurrence(value); 151 | if(options.allowDuplicates || firstOccurrence) { 152 | var 153 | ns = options.namespace, 154 | pre = ns+'-token', 155 | token = '
      '+ 156 | ''+value.trim()+''+ 157 | ''+options.xContent+''+ 158 | '
      '; 159 | list.append(token); 160 | } 161 | return input; 162 | }; 163 | pop = function () { 164 | return list.children().last().detach().data('token') || null; 165 | }; 166 | remove = function (value) { 167 | var tokens = list.children().filter(function() { 168 | return $(this).data('token') == value; // jshint ignore:line 169 | }).detach(); 170 | return tokens.length > 0 ? (tokens.length === 1 ? tokens.data('token') : tokens.length) : null; 171 | }; 172 | empty = function () { 173 | list.empty(); 174 | return input; 175 | }; 176 | get = function () { 177 | var 178 | i, 179 | tokenList = [], 180 | tokens = list.children(); 181 | for (i = 0; i < tokens.length; i++) { 182 | tokenList.push(tokens.eq(i).data('token').toString()); 183 | } 184 | return tokenList; 185 | }; 186 | destroy = function () { 187 | wrap.after(input).remove(); 188 | if (options.placeholder) { input.attr('placeholder', ''); } 189 | return input.removeClass(options.namespace+'-input'); 190 | }; 191 | callback = function () { 192 | (options.callback || $.noop)(input); 193 | return input; 194 | }; 195 | suggest = function (words, word) { 196 | word = word === undefined ? input.val() : word; 197 | var 198 | i, 199 | ns = options.namespace, 200 | re1 = new RegExp(escapeRegExp(word), 'i'), 201 | re2 = new RegExp('^'+escapeRegExp(word)+'$', 'i'), 202 | limit = options.numToSuggest || 1000, 203 | list = []; 204 | for (i = 0; word && i < words.length && list.length < limit; i++) { 205 | if (!words[i].match(re1) || (!options.allowDuplicates && !isFirstOccurrence(words[i], i))) { continue; } 206 | list.push('
    • '+words[i]+'
    • '); 208 | } 209 | suggestions.children('ul') 210 | .html(list.join('')) 211 | .end() 212 | [list.length ? 'addClass' : 'removeClass']('.'+ns+'-vis'); 213 | }; 214 | getMatch = function () { 215 | return suggestions.find('.'+options.namespace+'-sel').eq(0).text(); 216 | }; 217 | escapeRegExp = function (str) { 218 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 219 | }; 220 | isFirstOccurrence = function(data) { 221 | return get().indexOf(data) === -1; 222 | }; 223 | 224 | init (argElement); 225 | return { 226 | push: push, 227 | pop: pop, 228 | remove: remove, 229 | empty: empty, 230 | get: get, 231 | destroy: destroy, 232 | callback: callback 233 | }; 234 | }; 235 | 236 | 237 | // PUBLIC METHODS 238 | var methods = { 239 | init: function( options ) { 240 | if (this[0].nodeName !== 'INPUT') { 241 | console.error('Tokenizer requires an tag.'); 242 | return this; 243 | } 244 | return this.data(tokenizer, Tokenizer(this, options)); // jshint ignore:line 245 | }, 246 | push: function(value) { 247 | return this.data(tokenizer).push(value); 248 | }, 249 | pop: function() { 250 | return this.data(tokenizer).pop(); 251 | }, 252 | remove: function(value) { 253 | return this.data(tokenizer).remove(value); 254 | }, 255 | empty : function() { 256 | return this.data(tokenizer).empty(); 257 | }, 258 | get: function () { 259 | return this.data(tokenizer).get(); 260 | }, 261 | destroy: function () { 262 | return this.data(tokenizer).destroy(); 263 | }, 264 | callback: function () { 265 | return this.data(tokenizer).callback(); 266 | } 267 | }; 268 | 269 | 270 | // EXPORT PLUGIN 271 | $.fn[tokenizer] = function( method ) { 272 | if ( methods[method] ) { 273 | if (!this.data(tokenizer)) { console.error('Cannot call "'+method+'" - Tokenizer not initialized.'); } 274 | return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 )); 275 | } else if ( typeof method === 'object' || ! method ) { 276 | return methods.init.apply( this, arguments ); 277 | } else { 278 | console.error( 'Unknown tokenizer method ' + method + '.' ); 279 | } 280 | }; 281 | 282 | return $.fn[tokenizer]; 283 | })); 284 | --------------------------------------------------------------------------------