4 * Ajatus - Distributed CRM
5 * @requires jQuery v1.2.1
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
16 * Depends on jquery.bgiframe plugin to fix IE's problem with selects.
19 * @cat Plugins/Autocomplete
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
36 $.ajatus.widgets.includes = $.ajatus.widgets.includes || {};
37 $.ajatus.widgets.includes.tags = {
43 autofill_enabled: false,
47 $.ajatus.widgets.includes.tags.widget = function(input, options) {
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);
64 var previous_value = "";
66 var last_key_press_code;
67 var select = $.ajatus.widgets.includes.tags.widget.select(input, options, select_current);
69 input_element.keydown(function(event) {
70 last_key_press_code = event.keyCode;
72 switch(last_key_press_code) {
74 event.preventDefault();
75 if (select.visible()) {
82 event.preventDefault();
83 if (select.visible()) {
93 event.preventDefault();
94 if (select_current()) {
95 input_element.focus();
97 value = input_element.val();
98 if ( options.allow_create == true
105 input_element.trigger("result", [data]);
113 input_element.addClass('ajatus_tags_widget_loading');
114 clearTimeout(timeout);
115 timeout = setTimeout(on_change, options.delay);
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) {
122 event.preventDefault();
126 // track whether the field has focus, we shouldn't process any
127 // results if the field no longer has focus
132 }).click(function() {
133 // show select when clicking in a focused field
135 && !select.visible())
139 }).bind("result", function(event, data){
140 input_element.val('');
141 input_element.focus();
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);
152 function select_current() {
153 var selected = select.selected();
159 var v = selected.result;
162 input_element.val('');
165 input_element.trigger("result", [selected.data]);
169 function on_change(skip_prev_check) {
170 if (last_key_press_code == KEY.DEL) {
175 var current_value = input_element.val();
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];
184 if ( !skip_prev_check
185 && current_value == previous_value )
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);
202 function trim_words(value) {
207 var words = value.split($.trim(options.multiple_separator));
209 jQuery.each(words, function(i, value) {
211 result[i] = $.trim(value);
218 function last_word(value) {
219 if (! options.multiple) {
222 var words = trim_words(value);
223 return words[words.length - 1];
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 )
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);
238 function hide_results() {
239 clearTimeout(timeout);
240 timeout = setTimeout(hide_results_now, 200);
243 function hide_results_now() {
245 clearTimeout(timeout);
249 function receive_data(q, data) {
255 select.display(data, q);
256 autofill(q, data[0].title);
263 function request(term, context, success, failure) {
264 term = last_word(term); //.toLowerCase()
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)
271 results = $.ajatus.tags.search(term, context, options.result_limit);
272 if ( typeof results != 'object'
273 || results.length <= 0)
280 results = $.ajatus.widgets.includes.tags.widget.parse_raw_data(results);
281 success(term, results);
286 function stop_loading(had_errors) {
287 if (typeof had_errors == 'undefined') {
288 var had_errors = false;
291 input_element.removeClass('ajatus_tags_widget_loading');
295 input_element.removeClass('ajatus_tags_widget_idle');
296 input_element.addClass('ajatus_tags_widget_error');
298 input_element.removeClass('ajatus_tags_widget_error');
299 input_element.addClass('ajatus_tags_widget_idle');
304 $.ajatus.widgets.includes.tags.widget.parse_raw_data = function(data) {
306 $(data).each(function(i,t) {
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 || '') : ''
317 $(results).each(function(i){
318 var item = results[i];
320 parsed[parsed.length] = {
323 title: (item.context != '' ? item.context + ':' : '') + item.title,
324 context: item.context,
326 result: $.ajatus.widgets.includes.tags.widget.format_item(item)
334 $.ajatus.widgets.includes.tags.widget.select = function(input, options, select) {
336 ACTIVE: "ajatus_tags_widget_result_item_active"
339 // Create results holder element
340 var element = $('<div />')
344 .addClass('ajatus_tags_widget_results');
345 $(input).after(element);
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);
364 if (options.width > 0) {
365 element.css("width", options.width);
368 function target(event) {
369 var element = event.target;
370 while(element.tagName != "LI") {
371 element = element.parentNode;
376 function move_select(step) {
379 list_items.removeClass(CLASSES.ACTIVE).eq(active).addClass(CLASSES.ACTIVE);
382 function wrap_selection() {
384 active = list_items.size() - 1;
385 } else if (active >= list_items.size()) {
390 function limit_number_of_items(available) {
391 return (options.result_limit > 0) && (options.result_limit < available)
392 ? options.result_limit
396 function data_to_dom() {
397 var num = limit_number_of_items(data.length);
398 for (var i=0; i < num; i++) {
402 function highlight(value) {
403 return value.replace(new RegExp("(" + term + ")", "gi"), '<span style="font-weight: bold;">$1</span>');
405 $('<li />').html( highlight($.ajatus.widgets.includes.tags.widget.format_item(data[i].data)) ).appendTo(list);
408 list_items = list.find("li");
410 if ( options.select_first ) {
411 list_items.eq(0).addClass(CLASSES.ACTIVE);
417 display: function(d, q) {
434 visible : function() {
435 return element.is(":visible");
437 current: function() {
438 return this.visible() && (list_items.filter("." + CLASSES.ACTIVE)[0] || options.select_first && list_items[0]);
442 width: options.width > 0 ? options.width : ($(input).width() + 4)
445 selected: function() {
446 return data && data[active];
451 $.ajatus.widgets.includes.tags.widget.selections = function(input, options) {
453 HOVER: "ajatus_tags_widget_selection_item_hover",
454 DELETED: "ajatus_tags_widget_selection_item_deleted"
457 // Create selection holder element
458 var element = jQuery('<div />')
462 .addClass('ajatus_tags_widget_selections');
463 jQuery(input).before( element );
465 var list = jQuery('<ul />').appendTo(element);
470 function target(event) {
471 var element = event.target;
472 while(element.tagName != "LI") {
473 element = element.parentNode;
478 function gen_data_key(data) {
481 var value_parts = dt.split(':');
482 data.context = value_parts[0];
485 if (dt.match(/\=/)) {
486 var value_parts = dt.split('=');
487 data.value = value_parts[1];
492 return (typeof data.id != 'undefined' ? data.id : '') + dt + (typeof data.context != 'undefined' ? data.context : '') + (typeof data.value != 'undefined' ? data.value : '');
495 function can_add(data) {
496 if (options.selection_limit > 0) {
497 if (list_item.length == options.selection_limit) {
502 var existing = $.grep( list_items, function(n,i){
503 if ( typeof data.id != 'undefined'
506 return n.id == data.id;
508 if ( typeof data.context != 'undefined'
509 && ( typeof data.value == 'undefined'
510 || typeof data.value == ''))
512 return (n.title == data.title) && (n.context == data.context)
514 if ( typeof data.context == 'undefined'
515 && ( typeof data.value != 'undefined'
516 && typeof data.value != ''))
518 return (n.title == data.title) && (n.value == data.value)
520 if ( typeof data.context != 'undefined'
521 && ( typeof data.value != 'undefined'
522 && typeof data.value != ''))
524 return (n.title == data.title) && (n.context == data.context) && (n.value == data.value)
527 return n.title == data.title;
531 if (existing.length > 0) {
532 var key = gen_data_key(data);
533 $('#tag_'+key, list).highlightFade(800, 'yellow');
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)) {
558 var input_elem_name = "__tags_widget_selections";//" + data.title + "]";
560 var key = gen_data_key(data);
562 data.color = String(data.color).replace("#","");
564 var li_elem = jQuery('<li />')
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);
581 $(li_element).addClass(CLASSES.DELETED);
586 var span_elem = $('<span />')
588 $.ajatus.widgets.includes.tags.widget.format_item(data)
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);
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");
608 function restore(id) {
609 var input = $('#tag_'+id+' input', list);
610 input.attr({ value: input.attr('oldvalue') });
611 $('#tag_'+id).attr("deleted","false");
615 add_item: function(item, is_raw) {
616 if (typeof is_raw == 'undefined') {
621 var parsed = $.ajatus.widgets.includes.tags.widget.parse_raw_data([item]);
627 del_item: function(id) {
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);
640 } else if (field.setSelectionRange) {
641 field.setSelectionRange(start, end);
643 if (field.selectionStart) {
644 field.selectionStart = start;
645 field.selectionEnd = end;
651 $.ajatus.widgets.includes.tags.widget.format_item = function(item) {
654 if ( typeof item.context != 'undefined'
655 && item.context != '')
657 formatted += item.context + ':';
660 formatted += item.title;
662 if ( typeof item.value != 'undefined'
665 formatted += '=' + item.value;
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);
678 tags_add_item: function(item, is_raw) {
679 return this.trigger("add_selection_item", [item, is_raw]);
681 tags_remove_item: function(id) {
682 return this.trigger("remove_selection_item", [id]);