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