admin_assistant 1.0.4 → 2.0.0.pre1

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