Added handling for values
[ajatus.git] / js / widgets / includes / tags.js
blobbe935afae48c835bfd14c8f58b69b6fdc5c8fc0d
1 /*
2  * This file is part of
3  *
4  * Ajatus - Distributed CRM
5  * @requires jQuery v1.2.1
6  * 
7  * Copyright (c) 2007 Jerry Jalava <jerry.jalava@gmail.com>
8  * Copyright (c) 2007 Nemein Oy <http://nemein.com>
9  * Website: http://ajatus.info
10  * Licensed under the GPL license
11  * http://www.gnu.org/licenses/gpl.html
12  * 
13  */
15  /*
16   * Depends on jquery.bgiframe plugin to fix IE's problem with selects.
17   *
18   * @name tags_widget
19   * @cat Plugins/Autocomplete
20   * @type jQuery
21   * @param String|Object url an URL to data backend
22   * @param Map options Optional settings
23   * @option Number min_chars The minimum number of characters a user has to type before the autocompleter activates. Default: 1
24   * @option Number delay The delay in milliseconds that search waits after a keystroke before activating itself. Default: 400
25   * @option Object extra_params Extra parameters for the backend. Default: {}
26   * @option Boolean select_first If this is set to true, the first result will be automatically selected on tab/return. Default: true
27   * @option Number width Specify a custom width for the select box. Default: width of the input element
28   * @option Boolean autofill_enabled Fill the textinput while still selecting a value, replacing the value if more is typed or something else is selected. Default: false
29   * @option Number result_limit Limit the number of items in the results box. Is also send as a "limit" parameter to backend on request. Default: 10
30   * @option Number width Specify a custom width for the select box. Default: width of the input element
31   * @option Boolean allow_create If this is set to true, then when user presses tab it creates a tag from current input value if we don't have any results. Default: false
32   */
34 (function($){
35     
36     $.ajatus.widgets.includes = $.ajatus.widgets.includes || {};
37     $.ajatus.widgets.includes.tags = {
38         defaults: {
39                 min_chars: 2,
40                 delay: 300,
41                 select_first: true,
42                 result_limit: 10,
43                 autofill_enabled: false,
44                 allow_create: true
45         }
46     };
47     $.ajatus.widgets.includes.tags.widget = function(input, options) {
48         var KEY = {
49                 UP: 38,
50                 DOWN: 40,
51                 DEL: 46,
52                 TAB: 9,
53                 RETURN: 13,
54                 ESC: 27,
55                 COMMA: 188,
56                 SPACE: 32,
57                 BACKSPACE: 8
58         };
59         
60         var input_element = $(input).attr("autocomplete", "off").addClass('ajatus_tags_widget_input').addClass('ajatus_tags_widget_idle');
61         var selection_holder = $.ajatus.widgets.includes.tags.widget.selections(input, options);
62         
63         var timeout;
64         var previous_value = "";
65         var has_focus = 0;
66         var last_key_press_code;
67         var select = $.ajatus.widgets.includes.tags.widget.select(input, options, select_current);
68         
69         input_element.keydown(function(event) {
70                 last_key_press_code = event.keyCode;
72                 switch(last_key_press_code) {
73                         case KEY.UP:
74                                 event.preventDefault();
75                                 if (select.visible()) {
76                                         select.prev();
77                                 } else {
78                                         on_change(true);
79                                 }
80                         break;
81                         case KEY.DOWN:
82                                 event.preventDefault();
83                                 if (select.visible()) {
84                                         select.next();
85                                 } else {
86                                         on_change(true);
87                                 }
88                         break;
89                         case KEY.TAB:
90                         case KEY.RETURN:
91                         case KEY.SPACE:
92                         case KEY.COMMA:
93                                 event.preventDefault();
94                                 if (select_current()) {
95                                         input_element.focus();
96                                 } else {
97                                     value = input_element.val();
98                                     if (   options.allow_create == true
99                                         && value != '')
100                                     {
101                                         var data = {
102                                             title: value,
103                                             color: '8596b6'
104                                         };
105                                         input_element.trigger("result", [data]);
106                                     }
107                                 }
108                         break;
109                         case KEY.ESC:
110                                 select.hide();
111                         break;
112                         default:
113                                 input_element.addClass('ajatus_tags_widget_loading');
114                                 clearTimeout(timeout);
115                                 timeout = setTimeout(on_change, options.delay);
116                 }
117         }).keypress(function(event) {
118                 // having fun with opera - remove this binding and Opera submits the form when we select an entry via return
119                 switch(event.keyCode) {
120                     case KEY.TAB:
121                     case KEY:RETURN:
122                         event.preventDefault();
123                     break;
124             }
125         }).focus(function(){
126                 // track whether the field has focus, we shouldn't process any
127                 // results if the field no longer has focus
128                 has_focus++;
129         }).blur(function() {
130                 has_focus = 0;
131                 hide_results();
132         }).click(function() {
133                 // show select when clicking in a focused field
134                 if (   has_focus++ > 1
135                     && !select.visible())
136                 {
137                         on_change(true);
138                 }
139         }).bind("result", function(event, data){
140             input_element.val('');
141             input_element.focus();
142             stop_loading();
143             selection_holder.add_item(data);
144         }).bind("add_selection_item", function(event, item, is_raw){
145             selection_holder.add_item(item, is_raw);
146         }).bind("remove_selection_item", function(event, id){
147             selection_holder.del_item(id);
148         });
149         
150         hide_results_now();
151         
152         function select_current() {         
153                 var selected = select.selected();
154                 
155                 if (! selected) {
156                         return false;                       
157                 }
159                 var v = selected.result;
160                 previous_value = v;
162                 input_element.val('');
163                 input.focus();
164                 hide_results_now();
165                 input_element.trigger("result", [selected.data]);
166                 return true;
167         }
168         
169         function on_change(skip_prev_check) {               
170                 if (last_key_press_code == KEY.DEL) {
171                         select.hide();
172                         return;
173                 }
175                 var current_value = input_element.val();
176             
177             var context_key = '';
178             if (current_value.match(/:/)) {
179                 var value_parts = current_value.split(':');
180                 context_key = value_parts[0];
181                 current_value = value_parts[1];
182             }
184                 if (   !skip_prev_check
185                     && current_value == previous_value )
186                 {
187                         return;
188                 }
190                 previous_value = current_value;
192                 current_value = last_word(current_value);
193                 if (current_value.length >= options.min_chars) {                        
194                         current_value = current_value;//.toLowerCase();
195                         request(current_value, context_key, receive_data, stop_loading);
196                 } else {
197                         stop_loading();
198                         select.hide();
199                 }
200         }
201         
202         function trim_words(value) {
203                 if (! value) {
204                         return [""];
205                 }
206                 
207                 var words = value.split($.trim(options.multiple_separator));
208                 var result = [];
209                 jQuery.each(words, function(i, value) {
210                         if ($.trim(value)) {
211                                 result[i] = $.trim(value);                          
212                         }
213                 });
214                 
215                 return result;
216         }
217         
218         function last_word(value) {
219                 if (! options.multiple) {
220                         return value;                       
221                 }
222                 var words = trim_words(value);
223                 return words[words.length - 1];
224         }
225         
226         function autofill(q, value) {
227                 if (   options.autofill_enabled
228                     && (last_word(input_element.val()).toLowerCase() == q.toLowerCase())
229                     && last_key_press_code != KEY.BACKSPACE )
230                 {
231                         // fill in the value (keep the case the user has typed)
232                         input_element.val(input_element.val() + value.substring(last_word(previous_value).length));
233                         // select the portion of the value not typed by the user (so the next character will erase)
234                         $.ajatus.widgets.includes.tags.widget.move_selection(input, previous_value.length, previous_value.length + value.length);
235                 }
236         }
237         
238         function hide_results() {
239                 clearTimeout(timeout);
240                 timeout = setTimeout(hide_results_now, 200);
241         }
243         function hide_results_now() {
244                 select.hide();
245                 clearTimeout(timeout);
246                 stop_loading();
247         }
248         
249         function receive_data(q, data) {            
250                 if (   data
251                     && data.length
252                     && has_focus)
253                 {
254                         stop_loading();
255                         select.display(data, q);
256                         autofill(q, data[0].title);
257                         select.show();
258                 } else {
259                         hide_results_now();
260                 }
261         }
262         
263         function request(term, context, success, failure) {            
264                 term = last_word(term); //.toLowerCase()
265             
266             // Search first from cache.
267             var results = $.ajatus.tags.search_cache(term, context, options.result_limit);
268             if (   typeof results != 'object'
269                 || results.length <= 0)
270             {                            
271                 results = $.ajatus.tags.search(term, context, options.result_limit);
272                 if (   typeof results != 'object'
273                     || results.length <= 0)
274                 {
275                     failure(true);
276                     return false;
277                 }
278             }
279             
280             results = $.ajatus.widgets.includes.tags.widget.parse_raw_data(results);
281             success(term, results);
282             
283             return true;
284         }
286         function stop_loading(had_errors) {         
287             if (typeof had_errors == 'undefined') {
288                 var had_errors = false;
289             }
290             
291                 input_element.removeClass('ajatus_tags_widget_loading');
292                                 
293                 if (had_errors) {
294                     hide_results();
295                         input_element.removeClass('ajatus_tags_widget_idle');
296                         input_element.addClass('ajatus_tags_widget_error');
297                 } else {
298                         input_element.removeClass('ajatus_tags_widget_error');
299                         input_element.addClass('ajatus_tags_widget_idle');
300                 }
301         }
302     }
303     
304     $.ajatus.widgets.includes.tags.widget.parse_raw_data = function(data) {        
305         var results = [];
306         $(data).each(function(i,t) {
307             results[i] = {
308                 id: t.id || t._id,                
309                 title: t.value.title.val,
310                 color: t.value.title.widget.config.color || '8596b6',
311                 context: (typeof(t.value.title.widget.config['context']) != 'undefined') ? (t.value.title.widget.config.context || '') : '',
312                 value: (typeof(t.value.title.widget.config['value']) != 'undefined') ? (t.value.title.widget.config.value || '') : ''
313             };
314         });
316         var parsed = [];
317         $(results).each(function(i){
318             var item = results[i];
319             if (item) {
320                 parsed[parsed.length] = {
321                     data: item,
322                     id: item.id,
323                     title: (item.context != '' ? item.context + ':' : '') + item.title,
324                     context: item.context,
325                     value: item.value,
326                     result: $.ajatus.widgets.includes.tags.widget.format_item(item)
327                 };
328             }           
329         });
330         
331         return parsed;
332     }
333     
334     $.ajatus.widgets.includes.tags.widget.select = function(input, options, select) {
335         var CLASSES = {
336                 ACTIVE: "ajatus_tags_widget_result_item_active"
337         };
338         
339         // Create results holder element
340         var element = $('<div />')
341             .css({
342                 display: "none"
343             })
344                 .addClass('ajatus_tags_widget_results');
345         $(input).after(element);
346         
347         var list = $('<ul />').appendTo(element).mouseover(function(event) {
348                 active = $('li', list).removeClass(CLASSES.ACTIVE).index(target(event));
349                 $(target(event)).addClass(CLASSES.ACTIVE);
350         }).mouseout( function(event) {
351                 $(target(event)).removeClass(CLASSES.ACTIVE);
352         }).click(function(event) {
353                 $(target(event)).addClass(CLASSES.ACTIVE);
354                 select();
355                 input.focus();
356                 return false;
357         });
358         
359         var list_items,
360                 active = -1,
361                 data,
362                 term = "";
363         
364         if (options.width > 0) {
365                 element.css("width", options.width);
366         }
367         
368         function target(event) {
369                 var element = event.target;
370                 while(element.tagName != "LI") {
371                         element = element.parentNode;
372                 }
373                 return element;
374         }
375         
376         function move_select(step) {
377                 active += step;
378                 wrap_selection();
379                 list_items.removeClass(CLASSES.ACTIVE).eq(active).addClass(CLASSES.ACTIVE);
380         }
381         
382         function wrap_selection() {
383                 if (active < 0) {
384                         active = list_items.size() - 1;
385                 } else if (active >= list_items.size()) {
386                         active = 0;
387                 }
388         }
389         
390         function limit_number_of_items(available) {
391                 return (options.result_limit > 0) && (options.result_limit < available)
392                         ? options.result_limit
393                         : available;
394         }
395         
396         function data_to_dom() {
397                 var num = limit_number_of_items(data.length);
398                 for (var i=0; i < num; i++) {
399                         if (! data[i]) {
400                                 continue;
401                         }
402                         function highlight(value) {
403                                 return value.replace(new RegExp("(" + term + ")", "gi"), '<span style="font-weight: bold;">$1</span>');
404                         }
405                         $('<li />').html( highlight($.ajatus.widgets.includes.tags.widget.format_item(data[i].data)) ).appendTo(list);
406                 }
407                 
408                 list_items = list.find("li");
409                 
410                 if ( options.select_first ) {
411                         list_items.eq(0).addClass(CLASSES.ACTIVE);
412                         active = 0;
413                 }
414         }
415         
416         return {
417                 display: function(d, q) {
418                         data = d;
419                         term = q;
420                         list.empty();
421                         data_to_dom();
422                         list.bgiframe();
423                 },
424                 next: function() {
425                         move_select(1);
426                 },
427                 prev: function() {
428                         move_select(-1);
429                 },
430                 hide: function() {
431                         element.hide();
432                         active = -1;
433                 },
434                 visible : function() {
435                         return element.is(":visible");
436                 },
437                 current: function() {
438                         return this.visible() && (list_items.filter("." + CLASSES.ACTIVE)[0] || options.select_first && list_items[0]);
439                 },
440                 show: function() {
441                         element.css({
442                                 width: options.width > 0 ? options.width : ($(input).width() + 4)
443                         }).show();
444                 },
445                 selected: function() {
446                         return data && data[active];
447                 }
448         };
449     }
450     
451     $.ajatus.widgets.includes.tags.widget.selections = function(input, options) {
452         var CLASSES = {
453                 HOVER: "ajatus_tags_widget_selection_item_hover",
454                 DELETED: "ajatus_tags_widget_selection_item_deleted"
455         };
457         // Create selection holder element
458         var element = jQuery('<div />')
459             .css({
460                 display: "none"
461             })
462                 .addClass('ajatus_tags_widget_selections');
463         jQuery(input).before( element );
465         var list = jQuery('<ul />').appendTo(element);
467         var list_items = [],
468             has_content = false;
470         function target(event) {
471                 var element = event.target;             
472                 while(element.tagName != "LI") {
473                         element = element.parentNode;               
474                 }
475                 return element;
476         }
477         
478         function gen_data_key(data) {
479             var dt = data.title;
480             if (dt.match(/:/)) {
481                 var value_parts = dt.split(':');
482                 data.context = value_parts[0];
483                 dt = value_parts[1];
484             }
485             if (dt.match(/\=/)) {
486                 var value_parts = dt.split('=');
487                 data.value = value_parts[1];
488                 dt = value_parts[0];
489             }
490             data.title = dt;
491             
492             return (typeof data.id != 'undefined' ? data.id : '') + dt + (typeof data.context != 'undefined' ? data.context : '') + (typeof data.value != 'undefined' ? data.value : '');
493         }
495         function can_add(data) {
496             if (options.selection_limit > 0) {
497                 if (list_item.length == options.selection_limit) {
498                     return false;
499                 }
500             }
502             var existing = $.grep( list_items, function(n,i){
503                 if (   typeof data.id != 'undefined'
504                     && data.id != '')
505                 {
506                     return n.id == data.id;
507                 } else {
508                     if (   typeof data.context != 'undefined'
509                         && (   typeof data.value == 'undefined'
510                             || typeof data.value == ''))
511                     {
512                         return (n.title == data.title) && (n.context == data.context)
513                     }
514                     if (   typeof data.context == 'undefined'
515                         && (   typeof data.value != 'undefined'
516                             && typeof data.value != ''))
517                     {
518                         return (n.title == data.title) && (n.value == data.value)
519                     }
520                     if (   typeof data.context != 'undefined'
521                         && (   typeof data.value != 'undefined'
522                             && typeof data.value != ''))
523                     {
524                         return (n.title == data.title) && (n.context == data.context) && (n.value == data.value)
525                     }
526                     
527                     return n.title == data.title;
528                 }
529             });
530             
531             if (existing.length > 0) {
532                 var key = gen_data_key(data);
533                 $('#tag_'+key, list).highlightFade(800, 'yellow');
534                 return false;
535             }
537             return true;
538         }
540         function add(data)
541         {
542             // console.log('selections add');
543             // console.log('data.id: '+data.id);
544             // console.log('data.title: '+data.title);
545             // console.log('data.color: '+data.color);
546             // console.log('data.context: '+data.context);
547             // console.log('data.value: '+data.value);
549             if (! can_add(data)) {
550                 return false;
551             }
553             if (! has_content) {
554                 has_content = true;
555                 element.show();
556             }
558             var input_elem_name = "__tags_widget_selections";//" + data.title + "]";
559             
560             var key = gen_data_key(data);
561             
562             data.color = String(data.color).replace("#","");
564             var li_elem = jQuery('<li />')
565             .attr({
566                 id: 'tag_'+key
567             }).css({
568                 'background-color': '#'+data.color
569             }).mouseover( function(event) {
570                         active = $("li", list).removeClass(CLASSES.HOVER).index(target(event));
571                         $(target(event)).addClass(CLASSES.HOVER);
572                 }).mouseout( function(event) {
573                         $(target(event)).removeClass(CLASSES.HOVER);
574                 }).click(function(event) {
575                     var li_element = target(event);
576                     var current_class = $(li_element).attr("deleted");
577                     if ($(li_element).attr("deleted") == "true") {
578                         $(li_element).removeClass(CLASSES.DELETED);
579                         restore(key);                   
580                     } else {
581                         $(li_element).addClass(CLASSES.DELETED);
582                         remove(key);
583                     }
584                         return false;
585                 });
586             var span_elem = $('<span />')
587                 .html(
588                     $.ajatus.widgets.includes.tags.widget.format_item(data)
589                 ).appendTo(li_elem);
590             var input_elem = $('<input type="hidden" />').attr({
591                 name: input_elem_name,
592                 value: $.ajatus.converter.toJSON(data),
593                 id: 'tags_widget_tag_value_'+key
594             }).hide().appendTo(li_elem);
596             li_elem.appendTo(list);
598             list_items.push(data);
599         }
601         function remove(id) {
602             var input = $('#tag_'+id+' input', list);
603             input.attr({ oldvalue: input.attr('value') });
604             input.attr({ value: '' });
605             $('#tag_'+id).attr("deleted","true");
606         }
608         function restore(id) {
609             var input = $('#tag_'+id+' input', list);
610             input.attr({ value: input.attr('oldvalue') });
611             $('#tag_'+id).attr("deleted","false");
612         }
614         return {
615             add_item: function(item, is_raw) {
616                 if (typeof is_raw == 'undefined') {
617                     var is_raw = false;
618                 }
619                 
620                 if (is_raw) {
621                     var parsed = $.ajatus.widgets.includes.tags.widget.parse_raw_data([item]);
622                         add(parsed[0].data);
623                 } else {
624                         add(item);                  
625                 }
626             },
627             del_item: function(id) {
628                 remove(id);
629             }
630         }
631     }
632     
633     $.ajatus.widgets.includes.tags.widget.move_selection = function(field, start, end) {
634         if (field.createTextRange) {
635                 var selRange = field.createTextRange();
636                 selRange.collapse(true);
637                 selRange.moveStart("character", start);
638                 selRange.moveEnd("character", end);
639                 selRange.select();
640         } else if (field.setSelectionRange) {
641                 field.setSelectionRange(start, end);
642         } else {
643                 if (field.selectionStart) {
644                         field.selectionStart = start;
645                         field.selectionEnd = end;
646                 }
647         }
648         field.focus();
649     }
650     
651     $.ajatus.widgets.includes.tags.widget.format_item = function(item) {
652         var formatted = '';
653         
654         if (   typeof item.context != 'undefined'
655             && item.context != '')
656         {
657             formatted += item.context + ':';
658         }
659         
660         formatted += item.title;
661         
662         if (   typeof item.value != 'undefined'
663             && item.value != '')
664         {
665             formatted += '=' + item.value;
666         }        
667         
668         return formatted;   
669     }
670     
671     $.fn.extend({
672         tags: function(options) {
673             options = $.extend({}, $.ajatus.widgets.includes.tags.defaults, options);
674             return this.each(function(){
675                 new $.ajatus.widgets.includes.tags.widget(this, options);
676             });
677         },
678         tags_add_item: function(item, is_raw) {
679             return this.trigger("add_selection_item", [item, is_raw]);
680         },
681         tags_remove_item: function(id) {
682             return this.trigger("remove_selection_item", [id]);
683         }
684     });    
685 })(jQuery);