aboutsummaryrefslogtreecommitdiffstats
path: root/js
diff options
context:
space:
mode:
authorByron Jones <bjones@mozilla.com>2013-11-25 16:21:03 +0800
committerByron Jones <bjones@mozilla.com>2013-11-25 16:21:03 +0800
commitba0765bf4dc468815e4fa45509010c0cd675e5b2 (patch)
tree32d979d1172495770a326f87f294c6f10a1eaa68 /js
parent0aade93cebf4192b240347e092a7e53a62436ea2 (diff)
downloadbugs-ba0765bf4dc468815e4fa45509010c0cd675e5b2.tar
bugs-ba0765bf4dc468815e4fa45509010c0cd675e5b2.tar.gz
bugs-ba0765bf4dc468815e4fa45509010c0cd675e5b2.tar.bz2
bugs-ba0765bf4dc468815e4fa45509010c0cd675e5b2.tar.xz
bugs-ba0765bf4dc468815e4fa45509010c0cd675e5b2.zip
Bug 793963: add the ability to tag comments with arbitrary tags
r=dkl, a=glob
Diffstat (limited to 'js')
-rw-r--r--js/comment-tagging.js376
-rw-r--r--js/comments.js19
-rw-r--r--js/util.js25
3 files changed, 408 insertions, 12 deletions
diff --git a/js/comment-tagging.js b/js/comment-tagging.js
new file mode 100644
index 000000000..b700fe11d
--- /dev/null
+++ b/js/comment-tagging.js
@@ -0,0 +1,376 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+
+YAHOO.bugzilla.commentTagging = {
+ ctag_div : false,
+ ctag_add : false,
+ counter : 0,
+ min_len : 3,
+ max_len : 24,
+ tags_by_no: {},
+ nos_by_tag: {},
+ current_id: 0,
+ current_no: -1,
+ can_edit : false,
+ pending : {},
+
+ init : function(can_edit) {
+ this.can_edit = can_edit;
+ this.ctag_div = Dom.get('bz_ctag_div');
+ this.ctag_add = Dom.get('bz_ctag_add');
+ YAHOO.util.Event.on(this.ctag_add, 'keypress', this.onKeyPress);
+ YAHOO.util.Event.onDOMReady(function() {
+ YAHOO.bugzilla.commentTagging.updateCollapseControls();
+ });
+ if (!can_edit) return;
+
+ var ds = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ ds.connTimeout = 30000;
+ ds.connMethodPost = true;
+ ds.connXhrMode = "cancelStaleRequests";
+ ds.maxCacheEntries = 5;
+ ds.responseSchema = {
+ metaFields : { error: "error", jsonRpcId: "id"},
+ resultsList : "result"
+ };
+
+ var ac = new YAHOO.widget.AutoComplete('bz_ctag_add', 'bz_ctag_autocomp', ds);
+ ac.maxResultsDisplayed = 7;
+ ac.generateRequest = function(query) {
+ query = YAHOO.lang.trim(query);
+ YAHOO.bugzilla.commentTagging.last_query = query;
+ YAHOO.bugzilla.commentTagging.counter = YAHOO.bugzilla.commentTagging.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ return YAHOO.lang.JSON.stringify({
+ method : "Bug.search_comment_tags",
+ id : YAHOO.bugzilla.commentTagging.counter,
+ params : [ { query : query, limit : 10 } ]
+ });
+ };
+ ac.minQueryLength = this.min_len;
+ ac.autoHighlight = false;
+ ac.typeAhead = true;
+ ac.queryDelay = 0.5;
+ ac.dataReturnEvent.subscribe(function(type, args) {
+ args[0].autoHighlight = args[2].length == 1;
+ });
+ },
+
+ toggle : function(comment_id, comment_no) {
+ if (!this.ctag_div) return;
+ var tags_container = Dom.get('ct_' + comment_no);
+
+ if (this.current_id == comment_id) {
+ // hide
+ this.current_id = 0;
+ this.current_no = -1;
+ Dom.addClass(this.ctag_div, 'bz_default_hidden');
+ this.hideError();
+ window.focus();
+
+ } else {
+ // show or move
+ this.rpcRefresh(comment_id, comment_no);
+ this.current_id = comment_id;
+ this.current_no = comment_no;
+ this.ctag_add.value = '';
+ tags_container.parentNode.insertBefore(this.ctag_div, tags_container);
+ Dom.removeClass(this.ctag_div, 'bz_default_hidden');
+ Dom.removeClass(tags_container, 'bz_default_hidden');
+ var comment = Dom.get('comment_text_' + comment_no);
+ if (Dom.hasClass(comment, 'collapsed')) {
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ window.setTimeout(function() {
+ YAHOO.bugzilla.commentTagging.ctag_add.focus();
+ }, 50);
+ }
+ },
+
+ hideInput : function() {
+ if (this.current_id != 0) {
+ this.toggle(this.current_id, this.current_no);
+ }
+ this.hideError();
+ },
+
+ showError : function(comment_id, comment_no, error) {
+ var bz_ctag_error = Dom.get('bz_ctag_error');
+ var tags_container = Dom.get('ct_' + comment_no);
+ tags_container.parentNode.appendChild(bz_ctag_error, tags_container);
+ Dom.get('bz_ctag_error_msg').innerHTML = YAHOO.lang.escapeHTML(error);
+ Dom.removeClass(bz_ctag_error, 'bz_default_hidden');
+ },
+
+ hideError : function() {
+ Dom.addClass('bz_ctag_error', 'bz_default_hidden');
+ },
+
+ onKeyPress : function(evt) {
+ evt = evt || window.event;
+ var charCode = evt.charCode || evt.keyCode;
+ if (evt.keyCode == 27) {
+ // escape
+ YAHOO.bugzilla.commentTagging.hideInput();
+ YAHOO.util.Event.stopEvent(evt);
+
+ } else if (evt.keyCode == 13) {
+ // return
+ YAHOO.util.Event.stopEvent(evt);
+ var tags = YAHOO.bugzilla.commentTagging.ctag_add.value.split(/[ ,]/);
+ var comment_id = YAHOO.bugzilla.commentTagging.current_id;
+ var comment_no = YAHOO.bugzilla.commentTagging.current_no;
+ YAHOO.bugzilla.commentTagging.hideInput();
+ try {
+ YAHOO.bugzilla.commentTagging.add(comment_id, comment_no, tags);
+ } catch(e) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, e.message);
+ }
+ }
+ },
+
+ showTags : function(comment_id, comment_no, tags) {
+ // remove existing tags
+ var tags_container = Dom.get('ct_' + comment_no);
+ while (tags_container.hasChildNodes()) {
+ tags_container.removeChild(tags_container.lastChild);
+ }
+ // add tags
+ if (tags != '') {
+ if (typeof(tags) == 'string') {
+ tags = tags.split(',');
+ }
+ for (var i = 0, l = tags.length; i < l; i++) {
+ tags_container.appendChild(this.buildTagHtml(comment_id, comment_no, tags[i]));
+ }
+ }
+ // update tracking array
+ this.tags_by_no['c' + comment_no] = tags;
+ this.updateCollapseControls();
+ },
+
+ updateCollapseControls : function() {
+ var container = Dom.get('comment_tags_collapse_expand_container');
+ if (!container) return;
+ // build list of tags
+ this.nos_by_tag = {};
+ for (var id in this.tags_by_no) {
+ if (this.tags_by_no.hasOwnProperty(id)) {
+ for (var i = 0, l = this.tags_by_no[id].length; i < l; i++) {
+ var tag = this.tags_by_no[id][i].toLowerCase();
+ if (!this.nos_by_tag.hasOwnProperty(tag)) {
+ this.nos_by_tag[tag] = [];
+ }
+ this.nos_by_tag[tag].push(id);
+ }
+ }
+ }
+ var tags = [];
+ for (var tag in this.nos_by_tag) {
+ if (this.nos_by_tag.hasOwnProperty(tag)) {
+ tags.push(tag);
+ }
+ }
+ tags.sort();
+ if (tags.length) {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode('Comment Tags:'));
+ var ul = document.createElement('ul');
+ ul.id = 'comment_tags_collapse_expand';
+ div.appendChild(ul);
+ for (var i = 0, l = tags.length; i < l; i++) {
+ var tag = tags[i];
+ var li = document.createElement('li');
+ ul.appendChild(li);
+ var a = document.createElement('a');
+ li.appendChild(a);
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, tag) {
+ YAHOO.bugzilla.commentTagging.toggleCollapse(tag);
+ YAHOO.util.Event.stopEvent(evt);
+ }, tag);
+ li.appendChild(document.createTextNode(' (' + this.nos_by_tag[tag].length + ')'));
+ a.innerHTML = tag;
+ }
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(div);
+ } else {
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ }
+ },
+
+ toggleCollapse : function(tag) {
+ var nos = this.nos_by_tag[tag];
+ if (!nos) return;
+ toggle_all_comments('collapse');
+ for (var i = 0, l = nos.length; i < l; i++) {
+ var comment_no = nos[i].match(/\d+$/)[0];
+ var comment = Dom.get('comment_text_' + comment_no);
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ },
+
+ buildTagHtml : function(comment_id, comment_no, tag) {
+ var el = document.createElement('span');
+ Dom.setAttribute(el, 'id', 'ct_' + comment_no + '_' + tag);
+ Dom.addClass(el, 'bz_comment_tag');
+ if (this.can_edit) {
+ var a = document.createElement('a');
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, args) {
+ YAHOO.bugzilla.commentTagging.remove(args[0], args[1], args[2])
+ YAHOO.util.Event.stopEvent(evt);
+ }, [comment_id, comment_no, tag]);
+ a.appendChild(document.createTextNode('x'));
+ el.appendChild(a);
+ el.appendChild(document.createTextNode("\u00a0"));
+ }
+ el.appendChild(document.createTextNode(tag));
+ return el;
+ },
+
+ add : function(comment_id, comment_no, add_tags) {
+ // build list of current tags from html
+ var tags = new Array();
+ var spans = Dom.getElementsByClassName('bz_comment_tag', undefined, 'ct_' + comment_no);
+ for (var i = 0, l = spans.length; i < l; i++) {
+ tags.push(spans[i].textContent.substr(2));
+ }
+ // add new tags
+ var new_tags = new Array();
+ for (var i = 0, l = add_tags.length; i < l; i++) {
+ var tag = YAHOO.lang.trim(add_tags[i]);
+ // validation
+ if (tag == '')
+ continue;
+ if (tag.length < YAHOO.bugzilla.commentTagging.min_len)
+ throw new Error("Comment tags must be at least " + this.min_len + " characters.");
+ if (tag.length > YAHOO.bugzilla.commentTagging.max_len)
+ throw new Error("Comment tags cannot be longer than " + this.min_len + " characters.");
+ // append new tag
+ if (bz_isValueInArrayIgnoreCase(tags, tag))
+ continue;
+ new_tags.push(tag);
+ tags.push(tag);
+ }
+ tags.sort();
+ // update
+ this.showTags(comment_id, comment_no, tags);
+ this.rpcUpdate(comment_id, comment_no, new_tags, undefined);
+ },
+
+ remove : function(comment_id, comment_no, tag) {
+ var el = Dom.get('ct_' + comment_no + '_' + tag);
+ if (el) {
+ el.parentNode.removeChild(el);
+ this.rpcUpdate(comment_id, comment_no, undefined, [ tag ]);
+ }
+ },
+
+ // If multiple updates are triggered quickly, overlapping refresh events
+ // are generated. We ignore all events except the last one.
+ incPending : function(comment_id) {
+ if (this.pending['c' + comment_id] == undefined) {
+ this.pending['c' + comment_id] = 1;
+ } else {
+ this.pending['c' + comment_id]++;
+ }
+ },
+
+ decPending : function(comment_id) {
+ if (this.pending['c' + comment_id] != undefined)
+ this.pending['c' + comment_id]--;
+ },
+
+ hasPending : function(comment_id) {
+ return this.pending['c' + comment_id] != undefined
+ && this.pending['c' + comment_id] > 0;
+ },
+
+ rpcRefresh : function(comment_id, comment_no, noRefreshOnError) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, data.error.message, noRefreshOnError);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(
+ comment_id, comment_no, data.result.comments[comment_id].tags);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, res.responseText, noRefreshOnError);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.comments',
+ params: {
+ comment_ids: [ comment_id ],
+ include_fields: [ 'tags' ]
+ }
+ })
+ );
+ },
+
+ rpcUpdate : function(comment_id, comment_no, add, remove) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, data.error.message);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(comment_id, comment_no, data.result);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.update_comment_tags',
+ params: {
+ comment_id: comment_id,
+ add: add,
+ remove: remove
+ }
+ })
+ );
+ },
+
+ handleRpcError : function(comment_id, comment_no, message, noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, message);
+ if (!noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.rpcRefresh(comment_id, comment_no, true);
+ }
+ }
+}
diff --git a/js/comments.js b/js/comments.js
index acf5dbf9b..8d314de15 100644
--- a/js/comments.js
+++ b/js/comments.js
@@ -25,9 +25,9 @@ function toggle_comment_display(link, comment_id) {
var comment = document.getElementById('comment_text_' + comment_id);
var re = new RegExp(/\bcollapsed\b/);
if (comment.className.match(re))
- expand_comment(link, comment);
+ expand_comment(link, comment, comment_id);
else
- collapse_comment(link, comment);
+ collapse_comment(link, comment, comment_id);
}
function toggle_all_comments(action) {
@@ -44,20 +44,22 @@ function toggle_all_comments(action) {
var id = comments[i].id.match(/\d*$/);
var link = document.getElementById('comment_link_' + id);
if (action == 'collapse')
- collapse_comment(link, comment);
+ collapse_comment(link, comment, id);
else
- expand_comment(link, comment);
+ expand_comment(link, comment, id);
}
}
-function collapse_comment(link, comment) {
+function collapse_comment(link, comment, comment_id) {
link.innerHTML = "[+]";
YAHOO.util.Dom.addClass(comment, 'collapsed');
+ YAHOO.util.Dom.addClass('comment_tag_' + comment_id, 'collapsed');
}
-function expand_comment(link, comment) {
+function expand_comment(link, comment, comment_id) {
link.innerHTML = "[&minus;]";
YAHOO.util.Dom.removeClass(comment, 'collapsed');
+ YAHOO.util.Dom.removeClass('comment_tag_' + comment_id, 'collapsed');
}
function wrapReplyText(text) {
@@ -110,11 +112,12 @@ function wrapReplyText(text) {
/* This way, we are sure that browsers which do not support JS
* won't display this link */
-function addCollapseLink(count, title) {
+function addCollapseLink(count, collapsed, title) {
document.write(' <a href="#" class="bz_collapse_comment"' +
' id="comment_link_' + count +
'" onclick="toggle_comment_display(this, ' + count +
- '); return false;" title="' + title + '">[&minus;]<\/a> ');
+ '); return false;" title="' + title + '">[' +
+ (collapsed ? '+' : '&minus;') + ']<\/a> ');
}
function goto_add_comments( anchor ){
diff --git a/js/util.js b/js/util.js
index 6d1f88938..d3a34477b 100644
--- a/js/util.js
+++ b/js/util.js
@@ -125,10 +125,7 @@ function bz_overlayBelow(item, parent) {
*/
function bz_isValueInArray(aArray, aValue)
{
- var run = 0;
- var len = aArray.length;
-
- for ( ; run < len; run++) {
+ for (var run = 0, len = aArray.length ; run < len; run++) {
if (aArray[run] == aValue) {
return true;
}
@@ -138,6 +135,26 @@ function bz_isValueInArray(aArray, aValue)
}
/**
+ * Checks if a specified value is in the specified array by performing a
+ * case-insensitive comparison.
+ *
+ * @param aArray Array to search for the value.
+ * @param aValue Value to search from the array.
+ * @return Boolean; true if value is found in the array and false if not.
+ */
+function bz_isValueInArrayIgnoreCase(aArray, aValue)
+{
+ var re = new RegExp(aValue.replace(/([^A-Za-z0-9])/g, "\\$1"), 'i');
+ for (var run = 0, len = aArray.length ; run < len; run++) {
+ if (aArray[run].match(re)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
* Create wanted options in a select form control.
*
* @param aSelect Select form control to manipulate.