[2] | 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 | // |
---|
| 9 | function 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 | |
---|
| 20 | jQuery.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 | }; |
---|