PageRenderTime 28ms CodeModel.GetById 11ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/autocomplete_tagging.js

https://bitbucket.org/cistrome/cistrome-harvard/
JavaScript | 374 lines | 238 code | 53 blank | 83 comment | 30 complexity | e74665ba6964b8890d2f5f4e2c2ff19a MD5 | raw file
  1/**
  2* JQuery extension for tagging with autocomplete.
  3* @author: Jeremy Goecks
  4* @require: jquery.autocomplete plugin
  5*/
  6//
  7// Initialize "tag click functions" for tags.
  8//
  9function init_tag_click_function(tag_elt, click_func) {
 10    $(tag_elt).find('.tag-name').each( function() {
 11        $(this).click( function() {
 12            var tag_str = $(this).text();
 13            var tag_name_and_value = tag_str.split(":");
 14            click_func(tag_name_and_value[0], tag_name_and_value[1]);
 15            return true;            
 16        });
 17    });
 18}
 19
 20jQuery.fn.autocomplete_tagging = function(options) {
 21
 22    var defaults = {
 23        get_toggle_link_text_fn: function(tags) { 
 24            var text = "";
 25            var num_tags = obj_length(tags);
 26            if (num_tags > 0) {
 27                text = num_tags + (num_tags > 1 ? " Tags" : " Tag");
 28            } else {
 29                text = "Add tags";
 30            }
 31            return text;
 32        },
 33        tag_click_fn : function (name, value) {},
 34        editable: true,
 35        input_size: 20,
 36        in_form: false,
 37        tags : {},
 38        use_toggle_link: true,
 39        item_id: "",
 40        add_tag_img: "",
 41        add_tag_img_rollover: "",
 42        delete_tag_img: "",
 43        ajax_autocomplete_tag_url: "",
 44        ajax_retag_url: "",
 45        ajax_delete_tag_url: "",
 46        ajax_add_tag_url: ""
 47    };
 48
 49    var settings = jQuery.extend(defaults, options);
 50
 51    //
 52    // Initalize object's elements.
 53    //
 54
 55    // Get elements for this object. For this_obj, assume the last element with the id is the "this"; this is somewhat of a hack to address the problem 
 56    // that there may be two tagging elements for a single item if there are both community and individual tags for an element.
 57    var this_obj = $(this);
 58    var tag_area = this_obj.find('.tag-area');
 59    var toggle_link = this_obj.find('.toggle-link');
 60    var tag_input_field = this_obj.find('.tag-input');
 61    var add_tag_button = this_obj.find('.add-tag-button');
 62
 63    // Initialize toggle link.
 64    toggle_link.click( function() {
 65        // Take special actions depending on whether toggle is showing or hiding link.
 66        var after_toggle_fn;
 67        if (tag_area.is(":hidden")) {
 68            after_toggle_fn = function() {
 69                // If there are no tags, go right to editing mode by generating a click on the area.
 70                var num_tags = $(this).find('.tag-button').length;
 71                if (num_tags === 0) {
 72                    tag_area.click();
 73                }
 74            };
 75        } else {
 76            after_toggle_fn = function() {
 77                tag_area.blur();
 78            };
 79        }
 80        tag_area.slideToggle("fast", after_toggle_fn);
 81        return $(this);
 82    });
 83
 84    // Initialize tag input field.
 85    if (settings.editable) {
 86        tag_input_field.hide();
 87    }
 88    tag_input_field.keyup( function(e) {
 89        if ( e.keyCode === 27 ) {
 90            // Escape key
 91            $(this).trigger( "blur" );
 92        } else if (
 93            ( e.keyCode === 13 ) || // Return Key
 94            ( e.keyCode === 188 ) || // Comma
 95            ( e.keyCode === 32 ) // Space
 96        ) {   
 97            //
 98            // Check input.
 99            //
100
101            var new_value = this.value;
102
103            // Do nothing if return key was used to autocomplete.
104            if (return_key_pressed_for_autocomplete === true) {
105                return_key_pressed_for_autocomplete = false;
106                return false;
107            }
108
109            // Suppress space after a ":"
110            if ( new_value.indexOf(": ", new_value.length - 2) !== -1) {
111                this.value = new_value.substring(0, new_value.length-1);
112                return false;
113            }
114
115            // Remove trigger keys from input.
116            if ( (e.keyCode === 188) || (e.keyCode === 32) ) {
117                new_value = new_value.substring( 0 , new_value.length - 1 );
118            }
119            
120            // Trim whitespace.
121            new_value = $.trim(new_value);
122
123            // Too short?
124            if (new_value.length < 2) {
125                return false;
126            }
127
128            //
129            // New tag OK - apply it.
130            //
131
132            this.value = ""; // Reset text field now that tag is being added
133
134            // Add button for tag after all other tag buttons.
135            var new_tag_button = build_tag_button(new_value);
136            var tag_buttons = tag_area.children(".tag-button");
137            if (tag_buttons.length !== 0) {
138                var last_tag_button = tag_buttons.slice(tag_buttons.length-1);
139                last_tag_button.after(new_tag_button);
140            } else {
141                tag_area.prepend(new_tag_button);
142            }
143            
144            // Add tag to internal list.
145            var tag_name_and_value = new_value.split(":");
146            settings.tags[tag_name_and_value[0]] = tag_name_and_value[1];
147
148            // Update toggle link text.
149            var new_text = settings.get_toggle_link_text_fn(settings.tags);
150            toggle_link.text(new_text);
151
152            // Commit tag to server.
153            var zz = $(this);
154            $.ajax({
155                url: settings.ajax_add_tag_url,
156                data: { new_tag: new_value },
157                error: function() {
158                    // Failed. Roll back changes and show alert.
159                    new_tag_button.remove();
160                    delete settings.tags[tag_name_and_value[0]];
161                    var new_text = settings.get_toggle_link_text_fn(settings.tags);
162                    toggle_link.text(new_text);
163                    alert( "Add tag failed" );
164                },
165                success: function() {
166                    // Flush autocomplete cache because it's not out of date.
167                    // TODO: in the future, we could remove the particular item
168                    // that was chosen from the cache rather than flush it.
169                    zz.flushCache();
170                }
171            });
172
173            return false;
174        }
175    });
176
177    // Add autocomplete to input.
178    var format_item_func = function(key, row_position, num_rows, value, search_term) {
179        var tag_name_and_value = value.split(":");
180        return (tag_name_and_value.length === 1 ? tag_name_and_value[0] : tag_name_and_value[1]);
181    };
182    var autocomplete_options = { selectFirst: false, formatItem: format_item_func,
183            autoFill: false, highlight: false };
184    tag_input_field.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options);
185
186
187    // Initialize delete tag images for current tags.
188    this_obj.find('.delete-tag-img').each(function() {
189        init_delete_tag_image( $(this) );
190    });
191    
192
193    // Initialize tag click function.
194    init_tag_click_function($(this), settings.tag_click_fn);
195
196    // Initialize "add tag" button.
197    add_tag_button.click( function() {
198        $(this).hide();
199
200        // Clicking on button is the same as clicking on the tag area.
201        tag_area.click();
202        return false;
203    });
204    
205    //
206    // Set up tag area interactions; these are needed only if tags are editable.
207    //
208    if (settings.editable) {
209        // When the tag area blurs, go to "view tag" mode.
210        tag_area.bind("blur", function(e) {
211            if (obj_length(settings.tags) > 0) {
212                add_tag_button.show();
213                tag_input_field.hide();
214                tag_area.removeClass("active-tag-area");
215                tag_area.addClass("tooltip");
216            } else {
217                // No tags, so do nothing to ensure that input is still visible.
218            }
219        });
220
221        // On click, enable user to add tags.
222        tag_area.click( function(e) {
223            var is_active = $(this).hasClass("active-tag-area");
224
225            // If a "delete image" object was pressed and area is inactive, do nothing.
226            if ($(e.target).hasClass("delete-tag-img") && !is_active) {
227                return false;
228            }
229
230            // If a "tag name" object was pressed and area is inactive, do nothing.
231            if ($(e.target).hasClass("tag-name") && !is_active) {
232                return false;
233            }
234            
235            // Remove tooltip.
236            $(this).removeClass("tooltip");
237
238            // Hide add tag button, show tag_input field. Change background to show 
239            // area is active.
240            $(this).addClass("active-tag-area");
241            add_tag_button.hide();
242            tag_input_field.show();
243            tag_input_field.focus();
244
245            // Add handler to document that will call blur when the tag area is blurred;
246            // a tag area is blurred when a user clicks on an element outside the area.
247            var handle_document_click = function(e)  {
248                var check_click = function(tag_area, target) {
249                    var tag_area_id = tag_area.attr("id");
250                    // Blur the tag area if the element clicked on is not in the tag area.
251                    if (target !== tag_area) {
252                        tag_area.blur();
253                        $(window).unbind("click.tagging_blur");
254                        $(this).addClass("tooltip");
255                    }
256                };
257                check_click(tag_area, $(e.target));
258            };
259            // TODO: we should attach the click handler to all frames in order to capture
260            // clicks outside the frame that this element is in.
261            //window.parent.document.onclick = handle_document_click;
262            //var temp = $(window.parent.document.body).contents().find("iframe").html();
263            //alert(temp);
264            //$(document).parent().click(handle_document_click);
265            $(window).bind("click.tagging_blur", handle_document_click);
266
267            return false;
268        });
269    }
270
271    // If using toggle link, hide the tag area. Otherwise, show the tag area.
272    if (settings.use_toggle_link) {
273        tag_area.hide();
274    }
275
276    //
277    // Helper functions.
278    //
279
280    //  
281    // Collapse tag name + value into a single string.
282    //
283    function build_tag_str(tag_name, tag_value) {
284        return tag_name + ( tag_value ? ":" + tag_value : "");
285    }
286
287
288    // Initialize a "delete tag image": when click, delete tag from UI and send delete request to server.
289    function init_delete_tag_image(delete_img) {
290        $(delete_img).mouseenter( function () {
291            $(this).attr("src", settings.delete_tag_img_rollover);
292        });
293        $(delete_img).mouseleave( function () {
294            $(this).attr("src", settings.delete_tag_img);
295        });
296        $(delete_img).click( function () {
297            // Tag button is image's parent.
298            var tag_button = $(this).parent();
299
300            // Get tag name, value.
301            var tag_name_elt = tag_button.find(".tag-name").eq(0);
302            var tag_str = tag_name_elt.text();
303            var tag_name_and_value = tag_str.split(":");
304            var tag_name = tag_name_and_value[0];
305            var tag_value = tag_name_and_value[1];
306
307            var prev_button = tag_button.prev();
308            tag_button.remove();
309
310            // Remove tag from local list for consistency.
311            delete settings.tags[tag_name];
312
313            // Update toggle link text.
314            var new_text = settings.get_toggle_link_text_fn(settings.tags);
315            toggle_link.text(new_text);
316
317            // Delete tag.
318            $.ajax({
319                url: settings.ajax_delete_tag_url,
320                data: { tag_name: tag_name },
321                error: function() { 
322                    // Failed. Roll back changes and show alert.
323                    settings.tags[tag_name] = tag_value;
324                    if (prev_button.hasClass("tag-button")) {
325                        prev_button.after(tag_button);
326                    } else {
327                        tag_area.prepend(tag_button);
328                    }
329                    alert( "Remove tag failed" );
330
331                    toggle_link.text(settings.get_toggle_link_text_fn(settings.tags));
332
333                    // TODO: no idea why it's necessary to set this up again.
334                    delete_img.mouseenter( function () {
335                        $(this).attr("src", settings.delete_tag_img_rollover);
336                    });
337                    delete_img.mouseleave( function () {	
338                        $(this).attr("src", settings.delete_tag_img);
339                    });
340                },
341                success: function() {}
342            });
343
344            return true;
345        });
346    }
347
348    //
349    // Function that builds a tag button.
350    //
351    function build_tag_button(tag_str) {
352        // Build "delete tag" image.
353        var delete_img = $("<img/>").attr("src", settings.delete_tag_img).addClass("delete-tag-img");
354        init_delete_tag_image(delete_img);
355        
356        // Build tag button.
357        var tag_name_elt = $("<span>").text(tag_str).addClass("tag-name");
358        tag_name_elt.click( function() {
359            var tag_name_and_value = tag_str.split(":");
360            settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]);
361            return true;
362        });
363
364        var tag_button = $("<span></span>").addClass("tag-button");
365        tag_button.append(tag_name_elt);
366        // Allow delete only if element is editable.
367        if (settings.editable) {
368            tag_button.append(delete_img);
369        }
370
371        return tag_button;
372    }
373
374};