tokenizes 0.1

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,720 @@
1
+ // MODIFIED
2
+
3
+ /*
4
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
5
+ * Version 1.4.2
6
+ *
7
+ * Copyright (c) 2009 James Smith (http://loopj.com)
8
+ * Licensed jointly under the GPL and MIT licenses,
9
+ * choose which one suits your project best!
10
+ *
11
+ */
12
+
13
+ (function ($) {
14
+ // Default settings
15
+ var DEFAULT_SETTINGS = {
16
+ hintText: "Type in a search term",
17
+ noResultsText: "No results",
18
+ searchingText: "Searching...",
19
+ deleteText: "×",
20
+ searchDelay: 300,
21
+ minChars: 1,
22
+ tokenLimit: null,
23
+ jsonContainer: null,
24
+ method: "GET",
25
+ contentType: "json",
26
+ queryParam: "q",
27
+ tokenDelimiter: ",",
28
+ preventDuplicates: false,
29
+ prePopulate: null,
30
+ animateDropdown: true,
31
+ onResult: null,
32
+ onAdd: null,
33
+ onDelete: null
34
+ };
35
+
36
+ // Default classes to use when theming
37
+ var DEFAULT_CLASSES = {
38
+ tokenList: "token-input-list",
39
+ token: "token-input-token",
40
+ tokenDelete: "token-input-delete-token",
41
+ selectedToken: "token-input-selected-token",
42
+ highlightedToken: "token-input-highlighted-token",
43
+ dropdown: "token-input-dropdown",
44
+ dropdownItem: "token-input-dropdown-item",
45
+ dropdownItem2: "token-input-dropdown-item2",
46
+ selectedDropdownItem: "token-input-selected-dropdown-item",
47
+ inputToken: "token-input-input-token"
48
+ };
49
+
50
+ // Input box position "enum"
51
+ var POSITION = {
52
+ BEFORE: 0,
53
+ AFTER: 1,
54
+ END: 2
55
+ };
56
+
57
+ // Keys "enum"
58
+ var KEY = {
59
+ BACKSPACE: 8,
60
+ TAB: 9,
61
+ ENTER: 13,
62
+ ESCAPE: 27,
63
+ SPACE: 32,
64
+ PAGE_UP: 33,
65
+ PAGE_DOWN: 34,
66
+ END: 35,
67
+ HOME: 36,
68
+ LEFT: 37,
69
+ UP: 38,
70
+ RIGHT: 39,
71
+ DOWN: 40,
72
+ NUMPAD_ENTER: 108,
73
+ COMMA: 188
74
+ };
75
+
76
+
77
+ // Expose the .tokenInput function to jQuery as a plugin
78
+ $.fn.tokenInput = function (url_or_data, options) {
79
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
80
+
81
+ return this.each(function () {
82
+ new $.TokenList(this, url_or_data, settings);
83
+ });
84
+ };
85
+
86
+
87
+ // TokenList class for each input
88
+ $.TokenList = function (input, url_or_data, settings) {
89
+ //
90
+ // Initialization
91
+ //
92
+
93
+ // Configure the data source
94
+ if($.type(url_or_data) === "string") {
95
+ // Set the url to query against
96
+ settings.url = url_or_data;
97
+
98
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
99
+ if(settings.crossDomain === undefined) {
100
+ if(settings.url.indexOf("://") === -1) {
101
+ settings.crossDomain = false;
102
+ } else {
103
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
104
+ }
105
+ }
106
+ } else if($.type(url_or_data) === "array") {
107
+ // Set the local data to search through
108
+ settings.local_data = url_or_data;
109
+ }
110
+
111
+ // Build class titles
112
+ if(settings.classes) {
113
+ // Use custom class titles
114
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
115
+ } else if(settings.theme) {
116
+ // Use theme-suffixed default class titles
117
+ settings.classes = {};
118
+ $.each(DEFAULT_CLASSES, function(key, value) {
119
+ settings.classes[key] = value + "-" + settings.theme;
120
+ });
121
+ } else {
122
+ settings.classes = DEFAULT_CLASSES;
123
+ }
124
+
125
+
126
+ // Save the tokens
127
+ var saved_tokens = [];
128
+
129
+ // Keep track of the number of tokens in the list
130
+ var token_count = 0;
131
+
132
+ // Basic cache to save on db hits
133
+ var cache = new $.TokenList.Cache();
134
+
135
+ // Keep track of the timeout, old vals
136
+ var timeout;
137
+ var input_val;
138
+
139
+ // Create a new text input an attach keyup events
140
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
141
+ .css({
142
+ outline: "none"
143
+ })
144
+ .focus(function () {
145
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
146
+ show_dropdown_hint();
147
+ }
148
+ })
149
+ .blur(function () {
150
+ hide_dropdown();
151
+ })
152
+ .bind("keyup keydown blur update", resize_input)
153
+ .keydown(function (event) {
154
+ var previous_token;
155
+ var next_token;
156
+
157
+ switch(event.keyCode) {
158
+ case KEY.LEFT:
159
+ case KEY.RIGHT:
160
+ case KEY.UP:
161
+ case KEY.DOWN:
162
+ if(!$(this).val()) {
163
+ previous_token = input_token.prev();
164
+ next_token = input_token.next();
165
+
166
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
167
+ // Check if there is a previous/next token and it is selected
168
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
169
+ deselect_token($(selected_token), POSITION.BEFORE);
170
+ } else {
171
+ deselect_token($(selected_token), POSITION.AFTER);
172
+ }
173
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
174
+ // We are moving left, select the previous token if it exists
175
+ select_token($(previous_token.get(0)));
176
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
177
+ // We are moving right, select the next token if it exists
178
+ select_token($(next_token.get(0)));
179
+ }
180
+ } else {
181
+ var dropdown_item = null;
182
+
183
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
184
+ dropdown_item = $(selected_dropdown_item).next();
185
+ } else {
186
+ dropdown_item = $(selected_dropdown_item).prev();
187
+ }
188
+
189
+ if(dropdown_item.length) {
190
+ select_dropdown_item(dropdown_item);
191
+ }
192
+ return false;
193
+ }
194
+ break;
195
+
196
+ case KEY.BACKSPACE:
197
+ previous_token = input_token.prev();
198
+
199
+ if(!$(this).val().length) {
200
+ if(selected_token) {
201
+ delete_token($(selected_token));
202
+ } else if(previous_token.length) {
203
+ select_token($(previous_token.get(0)));
204
+ }
205
+
206
+ return false;
207
+ } else if($(this).val().length === 1) {
208
+ hide_dropdown();
209
+ } else {
210
+ // set a timeout just long enough to let this function finish.
211
+ setTimeout(function(){do_search();}, 5);
212
+ }
213
+ break;
214
+
215
+ case KEY.TAB:
216
+ case KEY.ENTER:
217
+ case KEY.NUMPAD_ENTER:
218
+ case KEY.COMMA:
219
+ if(selected_dropdown_item) {
220
+ add_token($(selected_dropdown_item));
221
+ return false;
222
+ }
223
+ break;
224
+
225
+ case KEY.ESCAPE:
226
+ hide_dropdown();
227
+ return true;
228
+
229
+ default:
230
+ if(String.fromCharCode(event.which)) {
231
+ // set a timeout just long enough to let this function finish.
232
+ setTimeout(function(){do_search();}, 5);
233
+ }
234
+ break;
235
+ }
236
+ });
237
+
238
+ // Keep a reference to the original input box
239
+ var hidden_input = $(input)
240
+ .hide()
241
+ .val("")
242
+ .focus(function () {
243
+ input_box.focus();
244
+ })
245
+ .blur(function () {
246
+ input_box.blur();
247
+ });
248
+
249
+ // Keep a reference to the selected token and dropdown item
250
+ var selected_token = null;
251
+ var selected_dropdown_item = null;
252
+
253
+ // The list to store the token items in
254
+ var token_list = $("<ul />")
255
+ .addClass(settings.classes.tokenList)
256
+ .click(function (event) {
257
+ var li = $(event.target).closest("li");
258
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
259
+ toggle_select_token(li);
260
+ } else {
261
+ // Deselect selected token
262
+ if(selected_token) {
263
+ deselect_token($(selected_token), POSITION.END);
264
+ }
265
+
266
+ // Focus input box
267
+ input_box.focus();
268
+ }
269
+ })
270
+ .mouseover(function (event) {
271
+ var li = $(event.target).closest("li");
272
+ if(li && selected_token !== this) {
273
+ li.addClass(settings.classes.highlightedToken);
274
+ }
275
+ })
276
+ .mouseout(function (event) {
277
+ var li = $(event.target).closest("li");
278
+ if(li && selected_token !== this) {
279
+ li.removeClass(settings.classes.highlightedToken);
280
+ }
281
+ })
282
+ .insertBefore(hidden_input);
283
+
284
+ // The token holding the input box
285
+ var input_token = $("<li />")
286
+ .addClass(settings.classes.inputToken)
287
+ .appendTo(token_list)
288
+ .append(input_box);
289
+
290
+ // The list to store the dropdown items in
291
+ var dropdown = $("<div>")
292
+ .addClass(settings.classes.dropdown)
293
+ .appendTo("body")
294
+ .hide();
295
+
296
+ // Magic element to help us resize the text input
297
+ var input_resizer = $("<tester/>")
298
+ .insertAfter(input_box)
299
+ .css({
300
+ position: "absolute",
301
+ top: -9999,
302
+ left: -9999,
303
+ width: "auto",
304
+ fontSize: input_box.css("fontSize"),
305
+ fontFamily: input_box.css("fontFamily"),
306
+ fontWeight: input_box.css("fontWeight"),
307
+ letterSpacing: input_box.css("letterSpacing"),
308
+ whiteSpace: "nowrap"
309
+ });
310
+
311
+ // Pre-populate list if items exist
312
+ hidden_input.val("");
313
+ li_data = settings.prePopulate || hidden_input.data("pre");
314
+ if(li_data && li_data.length) {
315
+ $.each(li_data, function (index, value) {
316
+ insert_token(value.id, value.title);
317
+ });
318
+ }
319
+
320
+
321
+
322
+ //
323
+ // Private functions
324
+ //
325
+
326
+ function resize_input() {
327
+ if(input_val === (input_val = input_box.val())) {return;}
328
+
329
+ // Enter new content into resizer and resize input accordingly
330
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
331
+ input_resizer.html(escaped);
332
+ input_box.width(input_resizer.width() + 30);
333
+ }
334
+
335
+ function is_printable_character(keycode) {
336
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
337
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
338
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
339
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
340
+ }
341
+
342
+ // Inner function to a token to the list
343
+ function insert_token(id, value) {
344
+ var this_token = $("<li><p>"+ value +"</p> </li>")
345
+ .addClass(settings.classes.token)
346
+ .insertBefore(input_token);
347
+
348
+ // The 'delete token' button
349
+ $("<span>" + settings.deleteText + "</span>")
350
+ .addClass(settings.classes.tokenDelete)
351
+ .appendTo(this_token)
352
+ .click(function () {
353
+ delete_token($(this).parent());
354
+ return false;
355
+ });
356
+
357
+ // Store data on the token
358
+ var token_data = {"id": id, "title": value};
359
+ $.data(this_token.get(0), "tokeninput", token_data);
360
+
361
+ // Save this token for duplicate checking
362
+ saved_tokens.push(token_data);
363
+
364
+ // Update the hidden input
365
+ var token_ids = $.map(saved_tokens, function (el) {
366
+ return el.id;
367
+ });
368
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
369
+
370
+ token_count += 1;
371
+
372
+ return this_token;
373
+ }
374
+
375
+ // Add a token to the token list based on user input
376
+ function add_token (item) {
377
+ var li_data = $.data(item.get(0), "tokeninput");
378
+ var callback = settings.onAdd;
379
+
380
+ // See if the token already exists and select it if we don't want duplicates
381
+ if(token_count > 0 && settings.preventDuplicates) {
382
+ var found_existing_token = null;
383
+ token_list.children().each(function () {
384
+ var existing_token = $(this);
385
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
386
+ if(existing_data && existing_data.id === li_data.id) {
387
+ found_existing_token = existing_token;
388
+ return false;
389
+ }
390
+ });
391
+
392
+ if(found_existing_token) {
393
+ select_token(found_existing_token);
394
+ input_token.insertAfter(found_existing_token);
395
+ input_box.focus();
396
+ return;
397
+ }
398
+ }
399
+
400
+ // Insert the new tokens
401
+ insert_token(li_data.id, li_data.title);
402
+
403
+ // Check the token limit
404
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
405
+ input_box.hide();
406
+ hide_dropdown();
407
+ return;
408
+ } else {
409
+ input_box.focus();
410
+ }
411
+
412
+ // Clear input box
413
+ input_box.val("");
414
+
415
+ // Don't show the help dropdown, they've got the idea
416
+ hide_dropdown();
417
+
418
+ // Execute the onAdd callback if defined
419
+ if($.isFunction(callback)) {
420
+ callback(li_data);
421
+ }
422
+ }
423
+
424
+ // Select a token in the token list
425
+ function select_token (token) {
426
+ token.addClass(settings.classes.selectedToken);
427
+ selected_token = token.get(0);
428
+
429
+ // Hide input box
430
+ input_box.val("");
431
+
432
+ // Hide dropdown if it is visible (eg if we clicked to select token)
433
+ hide_dropdown();
434
+ }
435
+
436
+ // Deselect a token in the token list
437
+ function deselect_token (token, position) {
438
+ token.removeClass(settings.classes.selectedToken);
439
+ selected_token = null;
440
+
441
+ if(position === POSITION.BEFORE) {
442
+ input_token.insertBefore(token);
443
+ } else if(position === POSITION.AFTER) {
444
+ input_token.insertAfter(token);
445
+ } else {
446
+ input_token.appendTo(token_list);
447
+ }
448
+
449
+ // Show the input box and give it focus again
450
+ input_box.focus();
451
+ }
452
+
453
+ // Toggle selection of a token in the token list
454
+ function toggle_select_token(token) {
455
+ var previous_selected_token = selected_token;
456
+
457
+ if(selected_token) {
458
+ deselect_token($(selected_token), POSITION.END);
459
+ }
460
+
461
+ if(previous_selected_token === token.get(0)) {
462
+ deselect_token(token, POSITION.END);
463
+ } else {
464
+ select_token(token);
465
+ }
466
+ }
467
+
468
+ // Delete a token from the token list
469
+ function delete_token (token) {
470
+ // Remove the id from the saved list
471
+ var token_data = $.data(token.get(0), "tokeninput");
472
+ var callback = settings.onDelete;
473
+
474
+ // Delete the token
475
+ token.remove();
476
+ selected_token = null;
477
+
478
+ // Show the input box and give it focus again
479
+ input_box.focus();
480
+
481
+ // Remove this token from the saved list
482
+ saved_tokens = $.grep(saved_tokens, function (val) {
483
+ return (val.id !== token_data.id);
484
+ });
485
+
486
+ // Update the hidden input
487
+ var token_ids = $.map(saved_tokens, function (el) {
488
+ return el.id;
489
+ });
490
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
491
+
492
+ token_count -= 1;
493
+
494
+ if(settings.tokenLimit !== null) {
495
+ input_box
496
+ .show()
497
+ .val("")
498
+ .focus();
499
+ }
500
+
501
+ // Execute the onDelete callback if defined
502
+ if($.isFunction(callback)) {
503
+ callback(token_data);
504
+ }
505
+ }
506
+
507
+ // Hide and clear the results dropdown
508
+ function hide_dropdown () {
509
+ dropdown.hide().empty();
510
+ selected_dropdown_item = null;
511
+ }
512
+
513
+ function show_dropdown() {
514
+ dropdown
515
+ .css({
516
+ position: "absolute",
517
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
518
+ left: $(token_list).offset().left,
519
+ zindex: 999
520
+ })
521
+ .show();
522
+ }
523
+
524
+ function show_dropdown_searching () {
525
+ if(settings.searchingText) {
526
+ dropdown.html("<p>"+settings.searchingText+"</p>");
527
+ show_dropdown();
528
+ }
529
+ }
530
+
531
+ function show_dropdown_hint () {
532
+ if(settings.hintText) {
533
+ dropdown.html("<p>"+settings.hintText+"</p>");
534
+ show_dropdown();
535
+ }
536
+ }
537
+
538
+ // Highlight the query part of the search term
539
+ function highlight_term(value, term) {
540
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
541
+ }
542
+
543
+ // Populate the results dropdown with some results
544
+ function populate_dropdown (query, results) {
545
+ if(results && results.length) {
546
+ dropdown.empty();
547
+ var dropdown_ul = $("<ul>")
548
+ .appendTo(dropdown)
549
+ .mouseover(function (event) {
550
+ select_dropdown_item($(event.target).closest("li"));
551
+ })
552
+ .mousedown(function (event) {
553
+ add_token($(event.target).closest("li"));
554
+ return false;
555
+ })
556
+ .hide();
557
+
558
+ $.each(results, function(index, value) {
559
+ var this_li = $("<li>" + highlight_term(value.title, query) + "</li>")
560
+ .appendTo(dropdown_ul);
561
+
562
+ if(index % 2) {
563
+ this_li.addClass(settings.classes.dropdownItem);
564
+ } else {
565
+ this_li.addClass(settings.classes.dropdownItem2);
566
+ }
567
+
568
+ if(index === 0) {
569
+ select_dropdown_item(this_li);
570
+ }
571
+
572
+ $.data(this_li.get(0), "tokeninput", {"id": value.id, "title": value.title});
573
+ });
574
+
575
+ show_dropdown();
576
+
577
+ if(settings.animateDropdown) {
578
+ dropdown_ul.slideDown("fast");
579
+ } else {
580
+ dropdown_ul.show();
581
+ }
582
+ } else {
583
+ if(settings.noResultsText) {
584
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
585
+ show_dropdown();
586
+ }
587
+ }
588
+ }
589
+
590
+ // Highlight an item in the results dropdown
591
+ function select_dropdown_item (item) {
592
+ if(item) {
593
+ if(selected_dropdown_item) {
594
+ deselect_dropdown_item($(selected_dropdown_item));
595
+ }
596
+
597
+ item.addClass(settings.classes.selectedDropdownItem);
598
+ selected_dropdown_item = item.get(0);
599
+ }
600
+ }
601
+
602
+ // Remove highlighting from an item in the results dropdown
603
+ function deselect_dropdown_item (item) {
604
+ item.removeClass(settings.classes.selectedDropdownItem);
605
+ selected_dropdown_item = null;
606
+ }
607
+
608
+ // Do a search and show the "searching" dropdown if the input is longer
609
+ // than settings.minChars
610
+ function do_search() {
611
+ var query = input_box.val().toLowerCase();
612
+
613
+ if(query && query.length) {
614
+ if(selected_token) {
615
+ deselect_token($(selected_token), POSITION.AFTER);
616
+ }
617
+
618
+ if(query.length >= settings.minChars) {
619
+ show_dropdown_searching();
620
+ clearTimeout(timeout);
621
+
622
+ timeout = setTimeout(function(){
623
+ run_search(query);
624
+ }, settings.searchDelay);
625
+ } else {
626
+ hide_dropdown();
627
+ }
628
+ }
629
+ }
630
+
631
+ // Do the actual search
632
+ function run_search(query) {
633
+ var cached_results = cache.get(query);
634
+ if(cached_results) {
635
+ populate_dropdown(query, cached_results);
636
+ } else {
637
+ // Are we doing an ajax search or local data search?
638
+ if(settings.url) {
639
+ // Extract exisiting get params
640
+ var ajax_params = {};
641
+ ajax_params.data = {};
642
+ if(settings.url.indexOf("?") > -1) {
643
+ var parts = settings.url.split("?");
644
+ ajax_params.url = parts[0];
645
+
646
+ var param_array = parts[1].split("&");
647
+ $.each(param_array, function (index, value) {
648
+ var kv = value.split("=");
649
+ ajax_params.data[kv[0]] = kv[1];
650
+ });
651
+ } else {
652
+ ajax_params.url = settings.url;
653
+ }
654
+
655
+ // Prepare the request
656
+ ajax_params.data[settings.queryParam] = query;
657
+ ajax_params.type = settings.method;
658
+ ajax_params.dataType = settings.contentType;
659
+ if(settings.crossDomain) {
660
+ ajax_params.dataType = "jsonp";
661
+ }
662
+
663
+ // Attach the success callback
664
+ ajax_params.success = function(results) {
665
+ if($.isFunction(settings.onResult)) {
666
+ results = settings.onResult.call(this, results);
667
+ }
668
+ cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
669
+
670
+ // only populate the dropdown if the results are associated with the active search query
671
+ if(input_box.val().toLowerCase() === query) {
672
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
673
+ }
674
+ };
675
+
676
+ // Make the request
677
+ $.ajax(ajax_params);
678
+ } else if(settings.local_data) {
679
+ // Do the search through local data
680
+ var results = $.grep(settings.local_data, function (row) {
681
+ return row.title.toLowerCase().indexOf(query.toLowerCase()) > -1;
682
+ });
683
+
684
+ populate_dropdown(query, results);
685
+ }
686
+ }
687
+ }
688
+ };
689
+
690
+ // Really basic cache for the results
691
+ $.TokenList.Cache = function (options) {
692
+ var settings = $.extend({
693
+ max_size: 500
694
+ }, options);
695
+
696
+ var data = {};
697
+ var size = 0;
698
+
699
+ var flush = function () {
700
+ data = {};
701
+ size = 0;
702
+ };
703
+
704
+ this.add = function (query, results) {
705
+ if(size > settings.max_size) {
706
+ flush();
707
+ }
708
+
709
+ if(!data[query]) {
710
+ size += 1;
711
+ }
712
+
713
+ data[query] = results;
714
+ };
715
+
716
+ this.get = function (query) {
717
+ return data[query];
718
+ };
719
+ };
720
+ }(jQuery));