[Pkg-mozext-commits] [wot] 195/226: WOT Groups: tags in RW, new message indicator on the top;
David Prévot
taffit at moszumanska.debian.org
Fri May 1 00:35:51 UTC 2015
This is an automated email from the git hooks/post-receive script.
taffit pushed a commit to branch master
in repository wot.
commit 511eb223d0d599c2cbf03931ee6faafa7238c75d
Author: Sergey Andryukhin <sorgoz at yandex.com>
Date: Thu Mar 6 10:30:40 2014 +0200
WOT Groups: tags in RW, new message indicator on the top;
---
content/api.js | 434 +++--
content/cache.js | 29 +-
content/config.js | 2 +-
content/core.js | 3 +
content/libs/bootstrap-tagautocomplete.js | 150 ++
content/libs/bootstrap-typeahead.js | 334 ++++
content/libs/caret-position.js | 80 +
content/libs/jquery-ui.min.js | 5 +
content/libs/rangy-core.js | 3020 +++++++++++++++++++++++++++++
content/overlay.xul | 1 +
content/ratingwindow.js | 66 +-
content/rw/proxies.js | 57 +-
content/rw/ratingwindow.html | 374 ++--
content/rw/ratingwindow.js | 1286 ++++++++++--
content/rw/wot.js | 76 +-
content/tools.js | 8 +
content/wg.js | 143 ++
locale/en-US/wot.properties | 13 +
skin/b/message.png | Bin 0 -> 1392 bytes
skin/ratingwindow.css | 508 ++++-
skin/typeahead.css | 238 +++
21 files changed, 6240 insertions(+), 587 deletions(-)
diff --git a/content/api.js b/content/api.js
index 4d02ba7..35eee16 100644
--- a/content/api.js
+++ b/content/api.js
@@ -1175,7 +1175,6 @@ var wot_keeper = {
},
store_by_name: function (target, name, obj) {
-// console.log("keeper.store_by_name()", target, name, data);
var keeper_data = wot_storage.get(this.STORAGE_NAME, {});
keeper_data[wot_keeper._fullname(target, name)] = obj;
wot_storage.set(this.STORAGE_NAME, keeper_data, true);
@@ -1226,142 +1225,188 @@ var wot_keeper = {
wot_modules.push({ name: "wot_keeper", obj: wot_keeper });
-var wot_api_comments = {
- server: "www.mywot.com",
- version: "1", // Comments API version
- PENDING_COMMENT_SID: "pending_comment.",
- PENDING_REMOVAL_SID: "pending_removal.",
- MAX_TRIES: 10, // maximum amount of tries to send a comment or remove a comment
- retrytimeout: {
- submit: 30 * 1000,
- remove: 20 * 1000
- },
- nonces: {}, // to know connection between nonce and target
-
- serialize: function (obj) {
- // prepare data to be sent using xmlhttprequest via POST
- var str = [];
- for(var p in obj){
- if (obj.hasOwnProperty(p)) {
- str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
- }
- }
- return str.join("&");
- },
+var wot_website_api = {
- call: function (apiname, options, params, on_error, on_success) {
- try {
- var _this = wot_api_comments,
- nonce = wot_crypto.nonce(),
- original_target = params.target;
+// server: "www.mywot.com",
+ server: "dev.mywot.com",
+ version: "1", // Comments API version
+ nonces: {}, // to know connection between nonce and target
- params = params || {};
- var post_params = {};
+ serialize: function (obj) {
+ // prepare data to be sent using xmlhttprequest via POST
+ var str = [];
+ for(var p in obj){
+ if (obj.hasOwnProperty(p)) {
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ }
+ }
+ return str.join("&");
+ },
- params.id = wot_prefs.witness_id;
- params.nonce = nonce;
- params.version = WOT_PLATFORM + "-" + WOT_VERSION;
+ call: function (apigroup, api_settings, apiname, options, params, on_error, on_success) {
+ try {
- options = options || { type: "GET" };
+ var allowed_groups = ["comment", "wg"];
- if (options.encryption) {
- params.target = wot_crypto.encrypt(wot_idn.utftoidn(params.target), nonce);
- }
+ if (allowed_groups.indexOf(apigroup) < 0) {
+ wot_tools.log("apigroup", apigroup, "is not allowed to call");
+ return false;
+ }
- var components = [];
+ var _this = wot_website_api,
+ nonce = wot_crypto.nonce(),
+ original_target = params.target;
- for (var i in params) {
- if (params[i] != null) {
- var param_name = i,
- param_value = params[i];
+ params = params || {};
+ var post_params = {};
- // Use a hash instead of the real value in the authenticated query
- if (options.hash && options.hash == i) {
- param_name = "SHA1";
- param_value = wot_hash.bintohex(wot_hash.sha1str(unescape( encodeURIComponent( params[i] )))); //wot_crypto.bintohex(wot_crypto.sha1.sha1str(unescape( encodeURIComponent( params[i] ))));
- }
+ params.id = wot_prefs.witness_id;
+ params.nonce = nonce;
+ params.version = WOT_PLATFORM + "-" + WOT_VERSION;
- components.push(param_name + "=" + encodeURIComponent(param_value));
- }
- }
+ options = options || { type: "GET" };
- var query_string = components.join("&"),
- path = "/api/" + _this.version + "/addon/comment/" + apiname,
- full_path = path + "?" + query_string;
+ if (options.encryption) {
+ params.target = wot_crypto.encrypt(wot_idn.utftoidn(params.target), nonce);
+ }
- if (options.authentication) {
- var auth = wot_crypto.authenticate(full_path);
+ var components = [];
- if (!auth || !components.length) {
- return false;
- }
- full_path += "&auth=" + auth;
- }
+ for (var i in params) {
+ if (params[i] != null) {
+ var param_name = i,
+ param_value = params[i];
- if (options.type == "POST") {
- post_params.query = full_path;
+ // Use a hash instead of the real value in the authenticated query
+ if (options.hash && options.hash == i) {
+ param_name = "SHA1";
+ param_value = wot_hash.bintohex(wot_hash.sha1str(unescape( encodeURIComponent( params[i] )))); //wot_crypto.bintohex(wot_crypto.sha1.sha1str(unescape( encodeURIComponent( params[i] ))));
+ }
- if (options.hash) {
- post_params[options.hash] = params[options.hash]; // submit the real value of the parameter that is authenticated as the hash
- }
- }
+ components.push(param_name + "=" + encodeURIComponent(param_value));
+ }
+ }
- // the add-on does NOT have permissions for httpS://www.mywot.com so we use http and own encryption
- var type = options.type ? options.type : "GET";
- var url = "http://" + this.server + (type == "POST" ? path : full_path);
+ var query_string = components.join("&"),
+ path = "/api/" + _this.version + "/addon/"+ apigroup +"/" + apiname,
+ full_path = path + "?" + query_string;
- _this.nonces[nonce] = original_target; // remember the link between nonce and target
+ if (options.authentication) {
+ var auth = wot_crypto.authenticate(full_path);
- var request = new XMLHttpRequest();
- request.open(type, url);
+ if (!auth || !components.length) {
+ return false;
+ }
+ full_path += "&auth=" + auth;
+ }
- request.onload = function (event) {
- if (!event || !event.target || event.target.status != 200 ||
- !event.target.responseText) {
- wot_tools.wdump("api.comments.call.error: url = " + url + ", status = " + event.target.status);
+ if (options.type == "POST") {
+ post_params.query = full_path;
- if (typeof(on_error) == "function") {
- on_error(request, event.target.status, {});
- }
- return;
- }
+ if (options.hash) {
+ post_params[options.hash] = params[options.hash]; // submit the real value of the parameter that is authenticated as the hash
+ }
+ }
+
+ // the add-on does NOT have permissions for httpS://www.mywot.com so we use http and own encryption
+ var type = options.type ? options.type : "GET";
+ var url = "http://" + wot_website_api.server + (type == "POST" ? path : full_path);
+
+ _this.nonces[nonce] = original_target; // remember the link between nonce and target
+
+ var request = new XMLHttpRequest();
+ request.open(type, url);
+
+ request.onload = function (event) {
+ if (!event || !event.target || event.target.status != 200 ||
+ !event.target.responseText) {
+ wot_tools.wdump("api.comments.call.error: url = " + url + ", status = " + event.target.status);
+
+ if (typeof(on_error) == "function") {
+ on_error(request, event.target.status, {});
+ }
+ return;
+ }
// wot_tools.wdump("api.comments.call.success: url = " + url + ", status = " + event.target.status);
- var data = JSON.parse(event.target.responseText);
+ var data = JSON.parse(event.target.responseText);
- if (typeof(on_success) == "function") {
- on_success(data, event.target.status, nonce);
- }
+ if (typeof(on_success) == "function") {
+ on_success(data, event.target.status, nonce);
+ }
- };
+ };
- var prepared_post_params = null;
+ var prepared_post_params = null;
- if (type == "POST") {
- prepared_post_params = _this.serialize(post_params);
- request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
- request.setRequestHeader("Content-length", prepared_post_params.length);
- request.setRequestHeader("Connection", "close");
- }
+ if (type == "POST") {
+ prepared_post_params = wot_website_api.serialize(post_params);
+ request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ request.setRequestHeader("Content-length", prepared_post_params.length);
+ request.setRequestHeader("Connection", "close");
+ }
+
+ request.send(prepared_post_params);
+ return true;
- request.send(prepared_post_params);
- return true;
+ } catch (e) {
+ wot_tools.wdump("wot_api_comments.call(): failed with " + e);
+ }
- } catch (e) {
- wot_tools.wdump("wot_api_comments.call(): failed with " + e);
- }
+ return false;
+
+ },
+
+ pull_nonce: function (nonce) {
- return false;
+ var _this = wot_website_api,
+ target = null;
+
+ if (_this.nonces[nonce]) {
+ target = _this.nonces[nonce];
+ delete _this.nonces[nonce];
+ }
+
+ return target;
+ },
+
+ is_error: function (error) {
+
+ var error_code = 0,
+ error_debug = "it is raining outside :(";
+
+ if (error instanceof Array && error.length > 1) {
+ error_code = error[0];
+ error_debug = error[1];
+ } else {
+ error_code = (error !== undefined ? error : 0);
+ }
+ if (error_code && error_code != WOT_SITEAPI_ERRORS.error_codes.COMMENT_NOT_FOUND) {
+ wot_tools.wdump("Error is returned:" + error_code + " / " + error_debug + " / " + error);
+ }
+
+ return error_code; // if not zero, than it is error
+ }
+
+};
+
+var wot_api_comments = {
+ PENDING_COMMENT_SID: "pending_comment.",
+ PENDING_REMOVAL_SID: "pending_removal.",
+ MAX_TRIES: 10, // maximum amount of tries to send a comment or remove a comment
+ retrytimeout: {
+ submit: 30 * 1000,
+ remove: 20 * 1000
},
get: function(target) {
var _this = wot_api_comments;
// wot_tools.wdump("wot_api_comments.get(target) " + target);
- if (target) {
- _this.call("get",
+ if (target && !wot_url.isprivate(target)) {
+ wot_website_api.call("comment", {}, "get",
{
encryption: true,
authentication: true
@@ -1393,10 +1438,13 @@ var wot_api_comments = {
});
// if params are given, it means we are on normal way of sending data (not on retrying)
- if (comment && votes) {
+ if (comment) {
state.comment_data.comment = comment;
state.comment_data.cid = comment_id || 0;
- state.comment_data.categories = votes;
+ if (votes) {
+ // since WG votes are not mandatory if there is at leat one hashtag
+ state.comment_data.categories = votes;
+ }
state.tries = 0;
}
@@ -1410,7 +1458,7 @@ var wot_api_comments = {
state.comment_data['target'] = target;
- _this.call("submit",
+ wot_website_api.call("comment", {}, "submit",
{
encryption: true,
authentication: true,
@@ -1457,7 +1505,7 @@ var wot_api_comments = {
wot_storage.set(pref_pending_name, state); // remember the submission
- _this.call("remove",
+ wot_website_api.call("comment", {}, "remove",
{
encryption: true,
authentication: true,
@@ -1516,59 +1564,45 @@ var wot_api_comments = {
}
},
- pull_nonce: function (nonce) {
-// wot_tools.wdump("wot_api_comments._pull_once(nonce) " + nonce);
-
- var _this = wot_api_comments,
- target = null;
-
- if (_this.nonces[nonce]) {
- target = _this.nonces[nonce];
- delete _this.nonces[nonce];
- }
-
- return target;
- },
-
- is_error: function (error) {
-// wot_tools.wdump("wot_api_comments.is_error(error)" + error);
-
- var error_code = 0,
- error_debug = "it is raining outside :(";
-
- if (error instanceof Array && error.length > 1) {
- error_code = error[0];
- error_debug = error[1];
- } else {
- error_code = (error !== undefined ? error : 0);
- }
-
- if (error_code && error_code != WOT_COMMENTS.error_codes.COMMENT_NOT_FOUND) {
- wot_tools.wdump("Error is returned:" + error_code + " / " + error_debug + " / " + error);
- }
-
- return error_code; // if not zero, than it is error
- },
-
on_get_comment_response: function (data) {
// wot_tools.wdump("wot_api_comments.on_get_comment_response(data)" + JSON.stringify(data));
// check whether error occured or data arrived
var _this = wot_api_comments,
nonce = data ? data.nonce : null, // to recover target from response
- target = _this.pull_nonce(nonce),
- error_code = target ? _this.is_error(data.error) : WOT_COMMENTS.error_codes.COMMENT_NOT_FOUND;
+ target = wot_website_api.pull_nonce(nonce),
+ error_code = target ? wot_website_api.is_error(data.error) : WOT_SITEAPI_ERRORS.error_codes.COMMENT_NOT_FOUND;
switch (error_code) {
- case WOT_COMMENTS.error_codes.SUCCESS:
- wot_cache.set_comment(target, data); // TODO: implement
+ case WOT_SITEAPI_ERRORS.error_codes.SUCCESS:
+ wot_cache.set_comment(target, data);
break;
- case WOT_COMMENTS.error_codes.COMMENT_NOT_FOUND:
+ case WOT_SITEAPI_ERRORS.error_codes.COMMENT_NOT_FOUND:
wot_cache.remove_comment(target); // remove the comment if it is cached
break;
default:
wot_cache.set_comment(target, { status: WOT_QUERY_ERROR, error_code: error_code });
}
+ var fail_errors = [ // the list of errors that won't give WOT Groups data
+ WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_FAILED,
+ WOT_SITEAPI_ERRORS.error_codes. AUTHENTICATION_REP_SERVER_ERROR,
+ WOT_SITEAPI_ERRORS.error_codes.NO_ACTION_DEFINED
+ ];
+
+ if (fail_errors.indexOf(error_code) < 0 && target) { // check for tags data (WOT Groups)
+ var tags = wot_api_tags.clean(data.wgtags),
+ wg_enabled = data.wg || false;
+
+ wot_wg.enable(wg_enabled);
+
+ wot_cache.set_param(target, "wg", {
+// wg: wg_enabled,
+ tags: tags
+ });
+ } else if (target) { // otherwise when got a failure code
+ wot_cache.remove_param(target, "wg");
+ }
+
wot_cache.set_captcha(!!data.captcha);
wot_rw.update_ratingwindow_comment();
@@ -1580,20 +1614,29 @@ var wot_api_comments = {
// wot_tools.wdump("wot_api_comments.on_submit_comment_response(data) " + data);
var _this = wot_api_comments,
nonce = data.nonce, // to recover target from response
- target = _this.pull_nonce(nonce),
- error_code = _this.is_error(data.error);
+ target = wot_website_api.pull_nonce(nonce),
+ error_code = wot_website_api.is_error(data.error);
switch (error_code) {
- case WOT_COMMENTS.error_codes.SUCCESS:
- wot_keeper.remove_comment(target); // delete the locally saved comment only on successful submit
+ case WOT_SITEAPI_ERRORS.error_codes.SUCCESS:
+ var local = wot_keeper.get_comment(target);
+
+ wot_keeper.remove_comment(target); // delete the locally saved comment only on successful submit
wot_cache.update_comment(target, { status: WOT_QUERY_OK, error_code: error_code });
wot_storage.clear(_this.PENDING_COMMENT_SID + target); // don't try to send again
+
+ if (local && local.comment) {
+ // extract tags and append them to the cached list of mytags
+ var mytags = wot_wg.extract_tags(local.comment);
+ wot_wg.append_mytags(mytags);
+ }
+
break;
// for these errors we should try again, because there is non-zero possibility of quantum glitches around
- case WOT_COMMENTS.error_codes.AUTHENTICATION_FAILED:
- case WOT_COMMENTS.error_codes.AUTHENTICATION_REP_SERVER_ERROR:
- case WOT_COMMENTS.error_codes.COMMENT_SAVE_FAILED:
+ case WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_FAILED:
+ case WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_REP_SERVER_ERROR:
+ case WOT_SITEAPI_ERRORS.error_codes.COMMENT_SAVE_FAILED:
wot_cache.update_comment(target, { status: WOT_QUERY_ERROR, error_code: error_code });
_this.retry("submit", [ target ]); // yeah, try it again, ddos own server ;)
break;
@@ -1613,20 +1656,20 @@ var wot_api_comments = {
var _this = wot_api_comments,
nonce = data.nonce, // to recover target from response
- target = _this.pull_nonce(nonce),
- error_code = _this.is_error(data.error);
+ target = wot_website_api.pull_nonce(nonce),
+ error_code = wot_website_api.is_error(data.error);
switch (error_code) {
- case WOT_COMMENTS.error_codes.SUCCESS:
+ case WOT_SITEAPI_ERRORS.error_codes.SUCCESS:
wot_cache.remove_comment(target);
wot_keeper.remove_comment(target);
wot_storage.clear(_this.PENDING_REMOVAL_SID + target);
break;
// some errors require retry due to singularity of the Universe
- case WOT_COMMENTS.error_codes.AUTHENTICATION_FAILED:
- case WOT_COMMENTS.error_codes.AUTHENTICATION_REP_SERVER_ERROR:
- case WOT_COMMENTS.error_codes.COMMENT_REMOVAL_FAILED:
+ case WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_FAILED:
+ case WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_REP_SERVER_ERROR:
+ case WOT_SITEAPI_ERRORS.error_codes.COMMENT_REMOVAL_FAILED:
wot_cache.update_comment(target, { status: WOT_QUERY_ERROR, error_code: error_code });
_this.retry("remove", [ target ]);
break;
@@ -1639,3 +1682,80 @@ var wot_api_comments = {
wot_rw.update_ratingwindow_comment(); // to update status "the website is commented by the user"
}
};
+
+var wot_api_tags = {
+ my: {
+ get_tags: function () {
+ wot_api_tags.get_tags("mytags", "getmytags");
+ }
+ },
+
+ popular: {
+ get_tags: function () {
+ wot_api_tags.get_tags("popular_tags", "getmastertags");
+ }
+ },
+
+ get_tags: function (core_keyword, method) {
+
+ try {
+
+ wot_website_api.call("wg",
+ {
+ version: wot_website_api.version
+ },
+ method,
+ {
+ encryption: true,
+ authentication: true
+ },
+ {},
+ function (err) {
+ wot_tools.log("api.get_tags() failed", err);
+ },
+ function (data) {
+ wot_api_tags._on_get_tags(data, core_keyword);
+ });
+
+ } catch (e) {
+ wot_tools.log("api.get_tags() failed", err);
+ }
+
+ },
+
+ _on_get_tags: function (data, core_keyword) {
+
+ var error_code = wot_website_api.is_error(data.error);
+
+ var fail_errors = [ // the list of errors that won't give WOT Groups data
+ WOT_SITEAPI_ERRORS.error_codes.AUTHENTICATION_FAILED,
+ WOT_SITEAPI_ERRORS.error_codes. AUTHENTICATION_REP_SERVER_ERROR,
+ WOT_SITEAPI_ERRORS.error_codes.NO_ACTION_DEFINED
+ ];
+
+ if (fail_errors.indexOf(error_code) < 0 && data.wgtags) { // check for tags data (WOT Groups)
+ var func = wot_wg['set_'+core_keyword];
+ if (typeof(func) == 'function') {
+ func(wot_api_tags.clean(data.wgtags));
+ }
+ }
+
+ wot_wg.enable(data.wg === true);
+ },
+
+ clean: function (tag_array) {
+ // clean tags from hash char is it's there and add tokens field
+ var tags = [];
+
+ if (tag_array instanceof Array) {
+ tags = tag_array.map(function (item) {
+ if (item.value) {
+ item.value = item.value.replace(/#/g, '');
+ }
+ return item;
+ });
+ }
+
+ return tags;
+ }
+};
diff --git a/content/cache.js b/content/cache.js
index 69ef589..08591e7 100644
--- a/content/cache.js
+++ b/content/cache.js
@@ -316,17 +316,30 @@ var wot_cache =
return !!wot_hashtable.get("captcha_required");
},
+ set_param: function (name, param, data) {
+ this.set(name, param, JSON.stringify(data));
+ },
+
+ get_param: function (name, param) {
+ var json_data = this.get(name, param),
+ data = {};
+ if (json_data) {
+ data = JSON.parse(json_data);
+ }
+
+ return data;
+ },
+
+ remove_param: function (name, param) {
+ this.remove(name, param);
+ },
+
get_comment: function (name) {
- var json_data = this.get(name, "comment"),
- data = {};
- if (json_data) {
- data = JSON.parse(json_data);
- }
- return data;
+ return this.get_param(name, "comment");
},
set_comment: function (name, comment_data) {
- this.set(name, "comment", JSON.stringify(comment_data));
+ this.set_param(name, "comment", comment_data);
},
update_comment: function (name, data) {
@@ -338,7 +351,7 @@ var wot_cache =
},
remove_comment: function (name) {
- this.remove(name, "comment");
+ this.remove_param(name, "comment");
},
add_target: function(nonce, target, islink)
diff --git a/content/config.js b/content/config.js
index 3f07581..a405a06 100644
--- a/content/config.js
+++ b/content/config.js
@@ -362,7 +362,7 @@ const WOT_URL_MENUMY = "menu-my";
const WOT_URL_BTN = "button";
const WOT_URL_CTX = "contextmenu";
-const WOT_COMMENTS = {
+const WOT_SITEAPI_ERRORS = {
error_codes: {
"0": "SUCCESS",
"1": "NO_ACTION_DEFINED",
diff --git a/content/core.js b/content/core.js
index 4ade052..75af7b4 100644
--- a/content/core.js
+++ b/content/core.js
@@ -665,6 +665,8 @@ var wot_core =
wot_api_update.send(forced_update);
+ wot_wg.update_tags(); // update user tags and popular tags
+
if (!wot_core.hostname || wot_url.isprivate(wot_core.hostname) ||
wot_url.isexcluded(wot_core.hostname)) {
/* Invalid or excluded hostname */
@@ -726,6 +728,7 @@ var wot_core =
return;
}
}
+
} catch (e) {
dump("wot_core.update: failed with " + e + "\n");
}
diff --git a/content/libs/bootstrap-tagautocomplete.js b/content/libs/bootstrap-tagautocomplete.js
new file mode 100644
index 0000000..1b44645
--- /dev/null
+++ b/content/libs/bootstrap-tagautocomplete.js
@@ -0,0 +1,150 @@
+/* =============================================================
+ * bootstrap-tagautocomplete.js v0.1
+ * http://sandglaz.github.com/bootstrap-tagautocomplete
+ * =============================================================
+ * Copyright 2013 Sandglaz, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* TAGAUTOCOMPLETE PUBLIC CLASS DEFINITION
+ * =============================== */
+
+ var Tagautocomplete = function (element, options) {
+ $.fn.typeahead.Constructor.call(this, element, options)
+ this.after = this.options.after || this.after
+ this.show = this.options.show || this.show
+ }
+
+ /* NOTE: TAGAUTOCOMPLETE EXTENDS BOOTSTRAP-TYPEAHEAD.js
+ ========================================== */
+
+ Tagautocomplete.prototype = $.extend({}, $.fn.typeahead.Constructor.prototype, {
+
+ constructor: Tagautocomplete
+
+ , select: function () {
+ var val = this.$menu.find('.active').attr('data-value')
+
+ var offset = this.updater(val).length - this.length_of_query;
+ var position = getCaretPosition(this.$element[0]) + offset
+
+ this.node.splitText(this.index_for_split);
+ this.node.nextSibling.splitText(this.length_of_query);
+ this.node.nextSibling.nodeValue=this.updater(val);
+
+ this.$element.change();
+
+ this.after();
+
+ setCaretPosition(this.$element[0], position)
+
+ return this.hide()
+ }
+
+ , after: function () {
+
+ }
+
+ , show: function () {
+
+ var pos = this.$element.position();
+ var height = this.$element[0].offsetHeight;
+
+ this.$menu
+ .appendTo('body')
+ .show()
+ .css({
+ position: "absolute",
+ top: pos.top + height + "px",
+ left: pos.left + "px"
+ });
+
+ this.shown = true
+ return this
+ }
+
+ , extractor: function () {
+ var query = this.query;
+ var position = getCaretPosition(this.$element[0]);
+ query = query.substring(0, position);
+ var regex = new RegExp("(^|\\s)([" + this.options.character + "][\\w-]*)$");
+ var result = regex.exec(query);
+ if(result && result[2])
+ return result[2].trim().toLowerCase();
+ return '';
+ }
+
+ , updater: function(item) {
+ return item+' ';
+ }
+
+ , matcher: function (item) {
+ var tquery = this.extractor();
+ if(!tquery) return false;
+
+ //setting the values that will be needed by select() here, because mouse clicks can change these values.
+ this.length_of_query = tquery.length
+ var range = window.getSelection().getRangeAt(0);
+ this.index_for_split = range.startOffset - this.length_of_query;
+ this.node = range.startContainer
+
+ return ~item.toLowerCase().indexOf(tquery)
+ }
+
+ , highlighter: function (item) {
+ var query = this.extractor().replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
+ return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
+ return '<strong>' + match + '</strong>'
+ })
+ }
+
+ })
+
+
+ /* TAGAUTOCOMPLETE PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.tagautocomplete
+
+ $.fn.tagautocomplete = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('tagautocomplete')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('tagautocomplete', (data = new Tagautocomplete(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.tagautocomplete.Constructor = Tagautocomplete
+
+ $.fn.tagautocomplete.defaults = $.extend($.fn.typeahead.defaults, {
+ character: '@'
+ })
+
+
+ /* TAGAUTOCOMPLETE NO CONFLICT
+ * =================== */
+
+ $.fn.tagautocomplete.noConflict = function () {
+ $.fn.tagautocomplete = old
+ return this
+ }
+
+}(window.jQuery);
diff --git a/content/libs/bootstrap-typeahead.js b/content/libs/bootstrap-typeahead.js
new file mode 100644
index 0000000..44cfc6e
--- /dev/null
+++ b/content/libs/bootstrap-typeahead.js
@@ -0,0 +1,334 @@
+/* =============================================================
+ * bootstrap-typeahead.js v3.0.0
+ * http://twitter.github.com/bootstrap/javascript.html#typeahead
+ * =============================================================
+ * Copyright 2012 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+
+!function($){
+
+ "use strict"; // jshint ;_;
+
+
+ /* TYPEAHEAD PUBLIC CLASS DEFINITION
+ * ================================= */
+
+ var Typeahead = function (element, options) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.typeahead.defaults, options)
+ this.matcher = this.options.matcher || this.matcher
+ this.sorter = this.options.sorter || this.sorter
+ this.highlighter = this.options.highlighter || this.highlighter
+ this.updater = this.options.updater || this.updater
+ this.source = this.options.source
+ this.$menu = $(this.options.menu)
+ this.shown = false
+ this.listen()
+ }
+
+ Typeahead.prototype = {
+
+ constructor: Typeahead
+
+ , select: function () {
+ var val = this.$menu.find('.active').attr('data-value')
+ this.$element
+ .val(this.updater(val))
+ .text(this.updater(val))
+ .change()
+ return this.hide()
+ }
+
+ , updater: function (item) {
+ return item
+ }
+
+ , show: function () {
+ var pos = $.extend({}, this.$element.position(), {
+ height: this.$element[0].offsetHeight
+ })
+
+ this.$menu
+ .insertAfter(this.$element)
+ .css({
+ top: pos.top + pos.height
+ , left: pos.left
+ })
+ .show()
+
+ this.shown = true
+ return this
+ }
+
+ , hide: function () {
+ this.$menu.hide()
+ this.shown = false
+ return this
+ }
+
+ , lookup: function (event) {
+ var items
+
+ this.query = this.$element.is("input") ? this.$element.val() : this.$element.text();
+
+ if (!this.query || this.query.length < this.options.minLength) {
+ return this.shown ? this.hide() : this
+ }
+
+ items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
+
+ return items ? this.process(items) : this
+ }
+
+ , process: function (items) {
+ var that = this
+
+ items = $.grep(items, function (item) {
+ return that.matcher(item)
+ })
+
+ items = this.sorter(items)
+
+ if (!items.length) {
+ return this.shown ? this.hide() : this
+ }
+
+ return this.render(items.slice(0, this.options.items)).show()
+ }
+
+ , matcher: function (item) {
+ return ~item.toLowerCase().indexOf(this.query.toLowerCase())
+ }
+
+ , sorter: function (items) {
+ var beginswith = []
+ , caseSensitive = []
+ , caseInsensitive = []
+ , item
+
+ while (item = items.shift()) {
+ if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
+ else if (~item.indexOf(this.query)) caseSensitive.push(item)
+ else caseInsensitive.push(item)
+ }
+
+ return beginswith.concat(caseSensitive, caseInsensitive)
+ }
+
+ , highlighter: function (item) {
+ var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
+ return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
+ return '<strong>' + match + '</strong>'
+ })
+ }
+
+ , render: function (items) {
+ var that = this
+
+ items = $(items).map(function (i, item) {
+ i = $(that.options.item).attr('data-value', item)
+ i.find('a').html(that.highlighter(item))
+ return i[0]
+ })
+
+ items.first().addClass('active')
+ this.$menu.html(items)
+ return this
+ }
+
+ , next: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.next()
+
+ if (!next.length) {
+ next = $(this.$menu.find('li')[0])
+ }
+
+ next.addClass('active')
+ }
+
+ , prev: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , prev = active.prev()
+
+ if (!prev.length) {
+ prev = this.$menu.find('li').last()
+ }
+
+ prev.addClass('active')
+ }
+
+ , listen: function () {
+ this.$element
+ .on('focus', $.proxy(this.focus, this))
+ .on('blur', $.proxy(this.blur, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ if (this.eventSupported('keydown')) {
+ this.$element.on('keydown', $.proxy(this.keydown, this))
+ }
+
+ this.$menu
+ .on('click', $.proxy(this.click, this))
+ .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
+ .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
+ }
+
+ , eventSupported: function(eventName) {
+ var isSupported = eventName in this.$element
+ if (!isSupported) {
+ this.$element.setAttribute(eventName, 'return;')
+ isSupported = typeof this.$element[eventName] === 'function'
+ }
+ return isSupported
+ }
+
+ , move: function (e) {
+ if (!this.shown) return
+
+ switch(e.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ case 27: // escape
+ e.preventDefault()
+ break
+
+ case 38: // up arrow
+ e.preventDefault()
+ this.prev()
+ break
+
+ case 40: // down arrow
+ e.preventDefault()
+ this.next()
+ break
+ }
+
+ e.stopPropagation()
+ }
+
+ , keydown: function (e) {
+ this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
+ this.move(e)
+ }
+
+ , keypress: function (e) {
+ if (this.suppressKeyPressRepeat) return
+ this.move(e)
+ }
+
+ , keyup: function (e) {
+ switch(e.keyCode) {
+ case 40: // down arrow
+ case 38: // up arrow
+ case 16: // shift
+ case 17: // ctrl
+ case 18: // alt
+ break
+
+ case 9: // tab
+ case 13: // enter
+ if (!this.shown) return
+ this.select()
+ break
+
+ case 27: // escape
+ if (!this.shown) return
+ this.hide()
+ break
+
+ default:
+ this.lookup()
+ }
+
+ }
+
+ , focus: function (e) {
+ this.focused = true
+ }
+
+ , blur: function (e) {
+ this.focused = false
+ if (!this.mousedover && this.shown) this.hide()
+ }
+
+ , click: function (e) {
+ e.stopPropagation()
+ e.preventDefault()
+ this.select()
+ this.$element.focus()
+ }
+
+ , mouseenter: function (e) {
+ this.mousedover = true
+ this.$menu.find('.active').removeClass('active')
+ $(e.currentTarget).addClass('active')
+ }
+
+ , mouseleave: function (e) {
+ this.mousedover = false
+ if (!this.focused && this.shown) this.hide()
+ }
+
+ }
+
+
+ /* TYPEAHEAD PLUGIN DEFINITION
+ * =========================== */
+
+ var old = $.fn.typeahead
+
+ $.fn.typeahead = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('typeahead')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.typeahead.defaults = {
+ source: []
+ , items: 8
+ , menu: '<ul class="typeahead dropdown-menu"></ul>'
+ , item: '<li><a href="#"></a></li>'
+ , minLength: 1
+ }
+
+ $.fn.typeahead.Constructor = Typeahead
+
+
+ /* TYPEAHEAD NO CONFLICT
+ * =================== */
+
+ $.fn.typeahead.noConflict = function () {
+ $.fn.typeahead = old
+ return this
+ }
+
+
+ /* TYPEAHEAD DATA-API
+ * ================== */
+
+ $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
+ var $this = $(this)
+ if ($this.data('typeahead')) return
+ $this.typeahead($this.data())
+ })
+
+}(window.jQuery);
diff --git a/content/libs/caret-position.js b/content/libs/caret-position.js
new file mode 100644
index 0000000..3bd1adc
--- /dev/null
+++ b/content/libs/caret-position.js
@@ -0,0 +1,80 @@
+/* =============================================================
+ * caret-position.js v1.0.0
+ * =============================================================
+ * Copyright 2013 Sandglaz, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+
+function getCharacterOffsetWithin(range, node) {
+ var treeWalker = document.createTreeWalker(
+ node,
+ NodeFilter.SHOW_TEXT,
+ function(node) {
+ var nodeRange = document.createRange();
+ nodeRange.selectNode(node);
+ return nodeRange.compareBoundaryPoints(Range.END_TO_END, range) < 1 ?
+ NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
+ },
+ false
+ );
+
+ var charCount = 0;
+ while (treeWalker.nextNode()) {
+ charCount += treeWalker.currentNode.length;
+ }
+ if (range.startContainer.nodeType == 3) {
+ charCount += range.startOffset;
+ }
+ return charCount;
+}
+
+function getCaretPosition(containerEl) {
+ var range = window.getSelection().getRangeAt(0);
+ return getCharacterOffsetWithin(range, containerEl)
+}
+
+function setCaretPosition(containerEl, index) {
+ var charIndex = 0, stop = {};
+
+ function traverseNodes(node) {
+ if (node.nodeType == 3) {
+ var nextCharIndex = charIndex + node.length;
+ if (index >= charIndex && index <= nextCharIndex) {
+ rangy.getSelection().collapse(node, index - charIndex);
+ throw stop;
+ }
+ charIndex = nextCharIndex;
+ }
+ // Count an empty element as a single character. The list below may not be exhaustive.
+ else if (node.nodeType == 1
+ && /^(input|br|img|col|area|link|meta|link|param|base)$/i.test(node.nodeName)) {
+ charIndex += 1;
+ } else {
+ var child = node.firstChild;
+ while (child) {
+ traverseNodes(child);
+ child = child.nextSibling;
+ }
+ }
+ }
+
+ try {
+ traverseNodes(containerEl);
+ } catch (ex) {
+ if (ex != stop) {
+ throw ex;
+ }
+ }
+}
\ No newline at end of file
diff --git a/content/libs/jquery-ui.min.js b/content/libs/jquery-ui.min.js
new file mode 100644
index 0000000..bc01354
--- /dev/null
+++ b/content/libs/jquery-ui.min.js
@@ -0,0 +1,5 @@
+/*! jQuery UI - v1.9.2 - 2012-11-23
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.sortable.js, jquery.ui.effect.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, [...]
+* Copyright 2012 jQuery Foundation and other contributors; Licensed MIT */
+(function(e,t){function i(t,n){var r,i,o,u=t.nodeName.toLowerCase();return"area"===u?(r=t.parentNode,i=r.name,!t.href||!i||r.nodeName.toLowerCase()!=="map"?!1:(o=e("img[usemap=#"+i+"]")[0],!!o&&s(o))):(/input|select|textarea|button|object/.test(u)?!t.disabled:"a"===u?t.href||n:n)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().andSelf().filter(function(){return e.css(this,"visibility")==="hidden"}).length}var n=0,r=/^ui-id-\d+$/;e.ui=e.ui||{};if(e.ui.version)return; [...]
\ No newline at end of file
diff --git a/content/libs/rangy-core.js b/content/libs/rangy-core.js
new file mode 100644
index 0000000..ff50a8c
--- /dev/null
+++ b/content/libs/rangy-core.js
@@ -0,0 +1,3020 @@
+/**
+ * @license Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Copyright 2011, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.1.2
+ * Build date: 30 May 2011
+ */
+var rangy = (function() {
+
+
+ var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
+
+ var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+ "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
+
+ var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
+ "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
+ "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
+
+ var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
+
+ // Subset of TextRange's full set of methods that we're interested in
+ var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
+ "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint"];
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Trio of functions taken from Peter Michaux's article:
+ // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
+ function isHostMethod(o, p) {
+ var t = typeof o[p];
+ return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
+ }
+
+ function isHostObject(o, p) {
+ return !!(typeof o[p] == OBJECT && o[p]);
+ }
+
+ function isHostProperty(o, p) {
+ return typeof o[p] != UNDEFINED;
+ }
+
+ // Creates a convenience function to save verbose repeated calls to tests functions
+ function createMultiplePropertyTest(testFunc) {
+ return function(o, props) {
+ var i = props.length;
+ while (i--) {
+ if (!testFunc(o, props[i])) {
+ return false;
+ }
+ }
+ return true;
+ };
+ }
+
+ // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
+ var areHostMethods = createMultiplePropertyTest(isHostMethod);
+ var areHostObjects = createMultiplePropertyTest(isHostObject);
+ var areHostProperties = createMultiplePropertyTest(isHostProperty);
+
+ var api = {
+ initialized: false,
+ supported: true,
+
+ util: {
+ isHostMethod: isHostMethod,
+ isHostObject: isHostObject,
+ isHostProperty: isHostProperty,
+ areHostMethods: areHostMethods,
+ areHostObjects: areHostObjects,
+ areHostProperties: areHostProperties
+ },
+
+ features: {},
+
+ modules: {},
+ config: {
+ alertOnWarn: false
+ }
+ };
+
+ function fail(reason) {
+ window.alert("Rangy not supported in your browser. Reason: " + reason);
+ api.initialized = true;
+ api.supported = false;
+ }
+
+ api.fail = fail;
+
+ function warn(msg) {
+ var warningMessage = "Rangy warning: " + msg;
+ if (api.config.alertOnWarn) {
+ window.alert(warningMessage);
+ } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
+ window.console.log(warningMessage);
+ }
+ }
+
+ api.warn = warn;
+
+ var initListeners = [];
+ var moduleInitializers = [];
+
+ // Initialization
+ function init() {
+ if (api.initialized) {
+ return;
+ }
+ var testRange;
+ var implementsDomRange = false, implementsTextRange = false;
+
+ // First, perform basic feature tests
+
+ if (isHostMethod(document, "createRange")) {
+ testRange = document.createRange();
+ if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
+ implementsDomRange = true;
+ }
+ testRange.detach();
+ }
+
+ var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
+
+ if (body && isHostMethod(body, "createTextRange")) {
+ testRange = body.createTextRange();
+ if (areHostMethods(testRange, textRangeMethods) && areHostProperties(testRange, textRangeProperties)) {
+ implementsTextRange = true;
+ }
+ }
+
+ if (!implementsDomRange && !implementsTextRange) {
+ fail("Neither Range nor TextRange are implemented");
+ }
+
+ api.initialized = true;
+ api.features = {
+ implementsDomRange: implementsDomRange,
+ implementsTextRange: implementsTextRange
+ };
+
+ // Initialize modules and call init listeners
+ var allListeners = moduleInitializers.concat(initListeners);
+ for (var i = 0, len = allListeners.length; i < len; ++i) {
+ try {
+ allListeners[i](api);
+ } catch (ex) {
+ if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
+ window.console.log("Init listener threw an exception. Continuing.", ex);
+ }
+
+ }
+ }
+ }
+
+ // Allow external scripts to initialize this library in case it's loaded after the document has loaded
+ api.init = init;
+
+ // Execute listener immediately if already initialized
+ api.addInitListener = function(listener) {
+ if (api.initialized) {
+ listener(api);
+ } else {
+ initListeners.push(listener);
+ }
+ };
+
+ var createMissingNativeApiListeners = [];
+
+ api.addCreateMissingNativeApiListener = function(listener) {
+ createMissingNativeApiListeners.push(listener);
+ };
+
+ function createMissingNativeApi(win) {
+ win = win || window;
+ init();
+
+ // Notify listeners
+ for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
+ createMissingNativeApiListeners[i](win);
+ }
+ }
+
+ api.createMissingNativeApi = createMissingNativeApi;
+
+ /**
+ * @constructor
+ */
+ function Module(name) {
+ this.name = name;
+ this.initialized = false;
+ this.supported = false;
+ }
+
+ Module.prototype.fail = function(reason) {
+ this.initialized = true;
+ this.supported = false;
+
+ throw new Error("Module '" + this.name + "' failed to load: " + reason);
+ };
+
+ Module.prototype.warn = function(msg) {
+ api.warn("Module " + this.name + ": " + msg);
+ };
+
+ Module.prototype.createError = function(msg) {
+ return new Error("Error in Rangy " + this.name + " module: " + msg);
+ };
+
+ api.createModule = function(name, initFunc) {
+ var module = new Module(name);
+ api.modules[name] = module;
+
+ moduleInitializers.push(function(api) {
+ initFunc(api, module);
+ module.initialized = true;
+ module.supported = true;
+ });
+ };
+
+ api.requireModules = function(modules) {
+ for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
+ moduleName = modules[i];
+ module = api.modules[moduleName];
+ if (!module || !(module instanceof Module)) {
+ throw new Error("Module '" + moduleName + "' not found");
+ }
+ if (!module.supported) {
+ throw new Error("Module '" + moduleName + "' not supported");
+ }
+ }
+ };
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Wait for document to load before running tests
+
+ var docReady = false;
+
+ var loadHandler = function(e) {
+
+ if (!docReady) {
+ docReady = true;
+ if (!api.initialized) {
+ init();
+ }
+ }
+ };
+
+ // Test whether we have window and document objects that we will need
+ if (typeof window == UNDEFINED) {
+ fail("No window found");
+ return;
+ }
+ if (typeof document == UNDEFINED) {
+ fail("No document found");
+ return;
+ }
+
+ if (isHostMethod(document, "addEventListener")) {
+ document.addEventListener("DOMContentLoaded", loadHandler, false);
+ }
+
+ // Add a fallback in case the DOMContentLoaded event isn't supported
+ if (isHostMethod(window, "addEventListener")) {
+ window.addEventListener("load", loadHandler, false);
+ } else if (isHostMethod(window, "attachEvent")) {
+ window.attachEvent("onload", loadHandler);
+ } else {
+ fail("Window does not have required addEventListener or attachEvent method");
+ }
+
+ return api;
+})();
+rangy.createModule("DomUtil", function(api, module) {
+
+ var UNDEF = "undefined";
+ var util = api.util;
+
+ // Perform feature tests
+ if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
+ module.fail("document missing a Node creation method");
+ }
+
+ if (!util.isHostMethod(document, "getElementsByTagName")) {
+ module.fail("document missing getElementsByTagName method");
+ }
+
+ var el = document.createElement("div");
+ if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
+ module.fail("Incomplete Element implementation");
+ }
+
+ var textNode = document.createTextNode("test");
+ if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
+ !util.areHostProperties(textNode, ["data"]))) {
+ module.fail("Incomplete Text Node implementation");
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
+ // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
+ // contains just the document as a single element and the value searched for is the document.
+ var arrayContains = /*Array.prototype.indexOf ?
+ function(arr, val) {
+ return arr.indexOf(val) > -1;
+ }:*/
+
+ function(arr, val) {
+ var i = arr.length;
+ while (i--) {
+ if (arr[i] === val) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ function getNodeIndex(node) {
+ var i = 0;
+ while( (node = node.previousSibling) ) {
+ i++;
+ }
+ return i;
+ }
+
+ function getCommonAncestor(node1, node2) {
+ var ancestors = [], n;
+ for (n = node1; n; n = n.parentNode) {
+ ancestors.push(n);
+ }
+
+ for (n = node2; n; n = n.parentNode) {
+ if (arrayContains(ancestors, n)) {
+ return n;
+ }
+ }
+
+ return null;
+ }
+
+ function isAncestorOf(ancestor, descendant, selfIsAncestor) {
+ var n = selfIsAncestor ? descendant : descendant.parentNode;
+ while (n) {
+ if (n === ancestor) {
+ return true;
+ } else {
+ n = n.parentNode;
+ }
+ }
+ return false;
+ }
+
+ function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
+ var p, n = selfIsAncestor ? node : node.parentNode;
+ while (n) {
+ p = n.parentNode;
+ if (p === ancestor) {
+ return n;
+ }
+ n = p;
+ }
+ return null;
+ }
+
+ function isCharacterDataNode(node) {
+ var t = node.nodeType;
+ return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
+ }
+
+ function insertAfter(node, precedingNode) {
+ var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
+ if (nextNode) {
+ parent.insertBefore(node, nextNode);
+ } else {
+ parent.appendChild(node);
+ }
+ return node;
+ }
+
+ function splitDataNode(node, index) {
+ var newNode;
+ if (node.nodeType == 3) {
+ newNode = node.splitText(index);
+ } else {
+ newNode = node.cloneNode();
+ newNode.deleteData(0, index);
+ node.deleteData(0, node.length - index);
+ insertAfter(newNode, node);
+ }
+ return newNode;
+ }
+
+ function getDocument(node) {
+ if (node.nodeType == 9) {
+ return node;
+ } else if (typeof node.ownerDocument != UNDEF) {
+ return node.ownerDocument;
+ } else if (typeof node.document != UNDEF) {
+ return node.document;
+ } else if (node.parentNode) {
+ return getDocument(node.parentNode);
+ } else {
+ throw new Error("getDocument: no document found for node");
+ }
+ }
+
+ function getWindow(node) {
+ var doc = getDocument(node);
+ if (typeof doc.defaultView != UNDEF) {
+ return doc.defaultView;
+ } else if (typeof doc.parentWindow != UNDEF) {
+ return doc.parentWindow;
+ } else {
+ throw new Error("Cannot get a window object for node");
+ }
+ }
+
+ function getIframeDocument(iframeEl) {
+ if (typeof iframeEl.contentDocument != UNDEF) {
+ return iframeEl.contentDocument;
+ } else if (typeof iframeEl.contentWindow != UNDEF) {
+ return iframeEl.contentWindow.document;
+ } else {
+ throw new Error("getIframeWindow: No Document object found for iframe element");
+ }
+ }
+
+ function getIframeWindow(iframeEl) {
+ if (typeof iframeEl.contentWindow != UNDEF) {
+ return iframeEl.contentWindow;
+ } else if (typeof iframeEl.contentDocument != UNDEF) {
+ return iframeEl.contentDocument.defaultView;
+ } else {
+ throw new Error("getIframeWindow: No Window object found for iframe element");
+ }
+ }
+
+ function getBody(doc) {
+ return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
+ }
+
+ function comparePoints(nodeA, offsetA, nodeB, offsetB) {
+ // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
+ var nodeC, root, childA, childB, n;
+ if (nodeA == nodeB) {
+
+ // Case 1: nodes are the same
+ return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
+ } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
+
+ // Case 2: node C (container B or an ancestor) is a child node of A
+ return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
+ } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
+
+ // Case 3: node C (container A or an ancestor) is a child node of B
+ return getNodeIndex(nodeC) < offsetB ? -1 : 1;
+ } else {
+
+ // Case 4: containers are siblings or descendants of siblings
+ root = getCommonAncestor(nodeA, nodeB);
+ childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
+ childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
+
+ if (childA === childB) {
+ // This shouldn't be possible
+
+ throw new Error("comparePoints got to case 4 and childA and childB are the same!");
+ } else {
+ n = root.firstChild;
+ while (n) {
+ if (n === childA) {
+ return -1;
+ } else if (n === childB) {
+ return 1;
+ }
+ n = n.nextSibling;
+ }
+ throw new Error("Should not be here!");
+ }
+ }
+ }
+
+ function inspectNode(node) {
+ if (!node) {
+ return "[No node]";
+ }
+ if (isCharacterDataNode(node)) {
+ return '"' + node.data + '"';
+ } else if (node.nodeType == 1) {
+ var idAttr = node.id ? ' id="' + node.id + '"' : "";
+ return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
+ } else {
+ return node.nodeName;
+ }
+ }
+
+ /**
+ * @constructor
+ */
+ function NodeIterator(root) {
+ this.root = root;
+ this._next = root;
+ }
+
+ NodeIterator.prototype = {
+ _current: null,
+
+ hasNext: function() {
+ return !!this._next;
+ },
+
+ next: function() {
+ var n = this._current = this._next;
+ var child, next;
+ if (this._current) {
+ child = n.firstChild;
+ if (child) {
+ this._next = child;
+ } else {
+ next = null;
+ while ((n !== this.root) && !(next = n.nextSibling)) {
+ n = n.parentNode;
+ }
+ this._next = next;
+ }
+ }
+ return this._current;
+ },
+
+ detach: function() {
+ this._current = this._next = this.root = null;
+ }
+ };
+
+ function createIterator(root) {
+ return new NodeIterator(root);
+ }
+
+ /**
+ * @constructor
+ */
+ function DomPosition(node, offset) {
+ this.node = node;
+ this.offset = offset;
+ }
+
+ DomPosition.prototype = {
+ equals: function(pos) {
+ return this.node === pos.node & this.offset == pos.offset;
+ },
+
+ inspect: function() {
+ return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
+ }/*,
+
+ isStartOfElementContent: function() {
+ var isCharacterData = isCharacterDataNode(this.node);
+ var el = isCharacterData ? this.node.parentNode : this.node;
+ return (el && el.nodeType == 1 && (isCharacterData ?
+ if (isCharacterDataNode(this.node) && !this.node.previousSibling && this.node.parentNode)
+ }*/
+ };
+
+ /**
+ * @constructor
+ */
+ function DOMException(codeName) {
+ this.code = this[codeName];
+ this.codeName = codeName;
+ this.message = "DOMException: " + this.codeName;
+ }
+
+ DOMException.prototype = {
+ INDEX_SIZE_ERR: 1,
+ HIERARCHY_REQUEST_ERR: 3,
+ WRONG_DOCUMENT_ERR: 4,
+ NO_MODIFICATION_ALLOWED_ERR: 7,
+ NOT_FOUND_ERR: 8,
+ NOT_SUPPORTED_ERR: 9,
+ INVALID_STATE_ERR: 11
+ };
+
+ DOMException.prototype.toString = function() {
+ return this.message;
+ };
+
+ api.dom = {
+ arrayContains: arrayContains,
+ getNodeIndex: getNodeIndex,
+ getCommonAncestor: getCommonAncestor,
+ isAncestorOf: isAncestorOf,
+ getClosestAncestorIn: getClosestAncestorIn,
+ isCharacterDataNode: isCharacterDataNode,
+ insertAfter: insertAfter,
+ splitDataNode: splitDataNode,
+ getDocument: getDocument,
+ getWindow: getWindow,
+ getIframeWindow: getIframeWindow,
+ getIframeDocument: getIframeDocument,
+ getBody: getBody,
+ comparePoints: comparePoints,
+ inspectNode: inspectNode,
+ createIterator: createIterator,
+ DomPosition: DomPosition
+ };
+
+ api.DOMException = DOMException;
+});rangy.createModule("DomRange", function(api, module) {
+ api.requireModules( ["DomUtil"] );
+
+
+ var dom = api.dom;
+ var DomPosition = dom.DomPosition;
+ var DOMException = api.DOMException;
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Utility functions
+
+ function isNonTextPartiallySelected(node, range) {
+ return (node.nodeType != 3) &&
+ (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
+ }
+
+ function getRangeDocument(range) {
+ return dom.getDocument(range.startContainer);
+ }
+
+ function dispatchEvent(range, type, args) {
+ var listeners = range._listeners[type];
+ if (listeners) {
+ for (var i = 0, len = listeners.length; i < len; ++i) {
+ listeners[i].call(range, {target: range, args: args});
+ }
+ }
+ }
+
+ function getBoundaryBeforeNode(node) {
+ return new DomPosition(node.parentNode, dom.getNodeIndex(node));
+ }
+
+ function getBoundaryAfterNode(node) {
+ return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
+ }
+
+ function getEndOffset(node) {
+ return dom.isCharacterDataNode(node) ? node.length : (node.childNodes ? node.childNodes.length : 0);
+ }
+
+ function insertNodeAtPosition(node, n, o) {
+ var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
+ if (dom.isCharacterDataNode(n)) {
+ if (o == n.length) {
+ dom.insertAfter(node, n);
+ } else {
+ n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
+ }
+ } else if (o >= n.childNodes.length) {
+ n.appendChild(node);
+ } else {
+ n.insertBefore(node, n.childNodes[o]);
+ }
+ return firstNodeInserted;
+ }
+
+ function cloneSubtree(iterator) {
+ var partiallySelected;
+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+ partiallySelected = iterator.isPartiallySelectedSubtree();
+
+ node = node.cloneNode(!partiallySelected);
+ if (partiallySelected) {
+ subIterator = iterator.getSubtreeIterator();
+ node.appendChild(cloneSubtree(subIterator));
+ subIterator.detach(true);
+ }
+
+ if (node.nodeType == 10) { // DocumentType
+ throw new DOMException("HIERARCHY_REQUEST_ERR");
+ }
+ frag.appendChild(node);
+ }
+ return frag;
+ }
+
+ function iterateSubtree(rangeIterator, func, iteratorState) {
+ var it, n;
+ iteratorState = iteratorState || { stop: false };
+ for (var node, subRangeIterator; node = rangeIterator.next(); ) {
+ //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
+ if (rangeIterator.isPartiallySelectedSubtree()) {
+ // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
+ // node selected by the Range.
+ if (func(node) === false) {
+ iteratorState.stop = true;
+ return;
+ } else {
+ subRangeIterator = rangeIterator.getSubtreeIterator();
+ iterateSubtree(subRangeIterator, func, iteratorState);
+ subRangeIterator.detach(true);
+ if (iteratorState.stop) {
+ return;
+ }
+ }
+ } else {
+ // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
+ // descendant
+ it = dom.createIterator(node);
+ while ( (n = it.next()) ) {
+ if (func(n) === false) {
+ iteratorState.stop = true;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ function deleteSubtree(iterator) {
+ var subIterator;
+ while (iterator.next()) {
+ if (iterator.isPartiallySelectedSubtree()) {
+ subIterator = iterator.getSubtreeIterator();
+ deleteSubtree(subIterator);
+ subIterator.detach(true);
+ } else {
+ iterator.remove();
+ }
+ }
+ }
+
+ function extractSubtree(iterator) {
+
+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+
+
+ if (iterator.isPartiallySelectedSubtree()) {
+ node = node.cloneNode(false);
+ subIterator = iterator.getSubtreeIterator();
+ node.appendChild(extractSubtree(subIterator));
+ subIterator.detach(true);
+ } else {
+ iterator.remove();
+ }
+ if (node.nodeType == 10) { // DocumentType
+ throw new DOMException("HIERARCHY_REQUEST_ERR");
+ }
+ frag.appendChild(node);
+ }
+ return frag;
+ }
+
+ function getNodesInRange(range, nodeTypes, filter) {
+ //log.info("getNodesInRange, " + nodeTypes.join(","));
+ var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
+ var filterExists = !!filter;
+ if (filterNodeTypes) {
+ regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
+ }
+
+ var nodes = [];
+ iterateSubtree(new RangeIterator(range, false), function(node) {
+ if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
+ nodes.push(node);
+ }
+ });
+ return nodes;
+ }
+
+ function inspect(range) {
+ var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
+ return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
+ dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // RangeIterator code borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
+
+ /**
+ * @constructor
+ */
+ function RangeIterator(range, clonePartiallySelectedTextNodes) {
+ this.range = range;
+ this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
+
+
+
+ if (!range.collapsed) {
+ this.sc = range.startContainer;
+ this.so = range.startOffset;
+ this.ec = range.endContainer;
+ this.eo = range.endOffset;
+ var root = range.commonAncestorContainer;
+
+ if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
+ this.isSingleCharacterDataNode = true;
+ this._first = this._last = this._next = this.sc;
+ } else {
+ this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
+ this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
+ this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
+ this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
+ }
+
+ }
+ }
+
+ RangeIterator.prototype = {
+ _current: null,
+ _next: null,
+ _first: null,
+ _last: null,
+ isSingleCharacterDataNode: false,
+
+ reset: function() {
+ this._current = null;
+ this._next = this._first;
+ },
+
+ hasNext: function() {
+ return !!this._next;
+ },
+
+ next: function() {
+ // Move to next node
+ var current = this._current = this._next;
+ if (current) {
+ this._next = (current !== this._last) ? current.nextSibling : null;
+
+ // Check for partially selected text nodes
+ if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
+ if (current === this.ec) {
+
+ (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
+ }
+ if (this._current === this.sc) {
+
+ (current = current.cloneNode(true)).deleteData(0, this.so);
+ }
+ }
+ }
+
+ return current;
+ },
+
+ remove: function() {
+ var current = this._current, start, end;
+
+ if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
+ start = (current === this.sc) ? this.so : 0;
+ end = (current === this.ec) ? this.eo : current.length;
+ if (start != end) {
+ current.deleteData(start, end - start);
+ }
+ } else {
+ if (current.parentNode) {
+ current.parentNode.removeChild(current);
+ } else {
+
+ }
+ }
+ },
+
+ // Checks if the current node is partially selected
+ isPartiallySelectedSubtree: function() {
+ var current = this._current;
+ return isNonTextPartiallySelected(current, this.range);
+ },
+
+ getSubtreeIterator: function() {
+ var subRange;
+ if (this.isSingleCharacterDataNode) {
+ subRange = this.range.cloneRange();
+ subRange.collapse();
+ } else {
+ subRange = new Range(getRangeDocument(this.range));
+ var current = this._current;
+ var startContainer = current, startOffset = 0, endContainer = current, endOffset = getEndOffset(current);
+
+ if (dom.isAncestorOf(current, this.sc, true)) {
+ startContainer = this.sc;
+ startOffset = this.so;
+ }
+ if (dom.isAncestorOf(current, this.ec, true)) {
+ endContainer = this.ec;
+ endOffset = this.eo;
+ }
+
+ updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
+ }
+ return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
+ },
+
+ detach: function(detachRange) {
+ if (detachRange) {
+ this.range.detach();
+ }
+ this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
+ }
+ };
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Exceptions
+
+ /**
+ * @constructor
+ */
+ function RangeException(codeName) {
+ this.code = this[codeName];
+ this.codeName = codeName;
+ this.message = "RangeException: " + this.codeName;
+ }
+
+ RangeException.prototype = {
+ BAD_BOUNDARYPOINTS_ERR: 1,
+ INVALID_NODE_TYPE_ERR: 2
+ };
+
+ RangeException.prototype.toString = function() {
+ return this.message;
+ };
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ /**
+ * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
+ * TODO: Look into making this a proper iterator, not requiring preloading everything first
+ * @constructor
+ */
+ function RangeNodeIterator(range, nodeTypes, filter) {
+ this.nodes = getNodesInRange(range, nodeTypes, filter);
+ this._next = this.nodes[0];
+ this._position = 0;
+ }
+
+ RangeNodeIterator.prototype = {
+ _current: null,
+
+ hasNext: function() {
+ return !!this._next;
+ },
+
+ next: function() {
+ this._current = this._next;
+ this._next = this.nodes[ ++this._position ];
+ return this._current;
+ },
+
+ detach: function() {
+ this._current = this._next = this.nodes = null;
+ }
+ };
+
+ var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
+ var rootContainerNodeTypes = [2, 9, 11];
+ var readonlyNodeTypes = [5, 6, 10, 12];
+ var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
+ var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
+
+ function createAncestorFinder(nodeTypes) {
+ return function(node, selfIsAncestor) {
+ var t, n = selfIsAncestor ? node : node.parentNode;
+ while (n) {
+ t = n.nodeType;
+ if (dom.arrayContains(nodeTypes, t)) {
+ return n;
+ }
+ n = n.parentNode;
+ }
+ return null;
+ };
+ }
+
+ function getRootContainer(node) {
+ var parent;
+ while ( (parent = node.parentNode) ) {
+ node = parent;
+ }
+ return node;
+ }
+
+ var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
+ var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
+ var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
+
+ function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
+ if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
+ throw new RangeException("INVALID_NODE_TYPE_ERR");
+ }
+ }
+
+ function assertNotDetached(range) {
+ if (!range.startContainer) {
+ throw new DOMException("INVALID_STATE_ERR");
+ }
+ }
+
+ function assertValidNodeType(node, invalidTypes) {
+ if (!dom.arrayContains(invalidTypes, node.nodeType)) {
+ throw new RangeException("INVALID_NODE_TYPE_ERR");
+ }
+ }
+
+ function assertValidOffset(node, offset) {
+ if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
+ throw new DOMException("INDEX_SIZE_ERR");
+ }
+ }
+
+ function assertSameDocumentOrFragment(node1, node2) {
+ if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
+ throw new DOMException("WRONG_DOCUMENT_ERR");
+ }
+ }
+
+ function assertNodeNotReadOnly(node) {
+ if (getReadonlyAncestor(node, true)) {
+ throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
+ }
+ }
+
+ function assertNode(node, codeName) {
+ if (!node) {
+ throw new DOMException(codeName);
+ }
+ }
+
+ function isOrphan(node) {
+ return !getDocumentOrFragmentContainer(node, true);
+ }
+
+ function isValidOffset(node, offset) {
+ return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
+ }
+
+ function assertRangeValid(range) {
+ if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
+ !isValidOffset(range.startContainer, range.startOffset) ||
+ !isValidOffset(range.endContainer, range.endOffset)) {
+ throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
+ }
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+ "commonAncestorContainer"];
+
+ var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
+ var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
+
+ function copyComparisonConstantsToObject(obj) {
+ obj.START_TO_START = s2s;
+ obj.START_TO_END = s2e;
+ obj.END_TO_END = e2e;
+ obj.END_TO_START = e2s;
+
+ obj.NODE_BEFORE = n_b;
+ obj.NODE_AFTER = n_a;
+ obj.NODE_BEFORE_AND_AFTER = n_b_a;
+ obj.NODE_INSIDE = n_i;
+ }
+
+ function copyComparisonConstants(constructor) {
+ copyComparisonConstantsToObject(constructor);
+ copyComparisonConstantsToObject(constructor.prototype);
+ }
+
+ function createPrototypeRange(constructor, boundaryUpdater, detacher) {
+ function createBeforeAfterNodeSetter(isBefore, isStart) {
+ return function(node) {
+ assertNotDetached(this);
+ assertValidNodeType(node, beforeAfterNodeTypes);
+ assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
+
+ var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
+ (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
+ };
+ }
+
+ function setRangeStart(range, node, offset) {
+ var ec = range.endContainer, eo = range.endOffset;
+ if (node !== range.startContainer || offset !== this.startOffset) {
+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
+ // is after the current end. In either case, collapse the range to the new position
+ if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
+ ec = node;
+ eo = offset;
+ }
+ boundaryUpdater(range, node, offset, ec, eo);
+ }
+ }
+
+ function setRangeEnd(range, node, offset) {
+ var sc = range.startContainer, so = range.startOffset;
+ if (node !== range.endContainer || offset !== this.endOffset) {
+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
+ // is after the current end. In either case, collapse the range to the new position
+ if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
+ sc = node;
+ so = offset;
+ }
+ boundaryUpdater(range, sc, so, node, offset);
+ }
+ }
+
+ function setRangeStartAndEnd(range, node, offset) {
+ if (node !== range.startContainer || offset !== this.startOffset || node !== range.endContainer || offset !== this.endOffset) {
+ boundaryUpdater(range, node, offset, node, offset);
+ }
+ }
+
+ function createRangeContentRemover(remover) {
+ return function() {
+ assertNotDetached(this);
+ assertRangeValid(this);
+
+ var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
+
+ var iterator = new RangeIterator(this, true);
+
+ // Work out where to position the range after content removal
+ var node, boundary;
+ if (sc !== root) {
+ node = dom.getClosestAncestorIn(sc, root, true);
+ boundary = getBoundaryAfterNode(node);
+ sc = boundary.node;
+ so = boundary.offset;
+ }
+
+ // Check none of the range is read-only
+ iterateSubtree(iterator, assertNodeNotReadOnly);
+
+ iterator.reset();
+
+ // Remove the content
+ var returnValue = remover(iterator);
+ iterator.detach();
+
+ // Move to the new position
+ boundaryUpdater(this, sc, so, sc, so);
+
+ return returnValue;
+ };
+ }
+
+ constructor.prototype = {
+ attachListener: function(type, listener) {
+ this._listeners[type].push(listener);
+ },
+
+ setStart: function(node, offset) {
+ assertNotDetached(this);
+ assertNoDocTypeNotationEntityAncestor(node, true);
+ assertValidOffset(node, offset);
+
+ setRangeStart(this, node, offset);
+ },
+
+ setEnd: function(node, offset) {
+ assertNotDetached(this);
+ assertNoDocTypeNotationEntityAncestor(node, true);
+ assertValidOffset(node, offset);
+
+ setRangeEnd(this, node, offset);
+ },
+
+ setStartBefore: createBeforeAfterNodeSetter(true, true),
+ setStartAfter: createBeforeAfterNodeSetter(false, true),
+ setEndBefore: createBeforeAfterNodeSetter(true, false),
+ setEndAfter: createBeforeAfterNodeSetter(false, false),
+
+ collapse: function(isStart) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ if (isStart) {
+ boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
+ } else {
+ boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
+ }
+ },
+
+ selectNodeContents: function(node) {
+ // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
+ // could be taken to mean only its children. However, browsers implement this the same as selectNode for
+ // text nodes, so I shall do likewise
+ assertNotDetached(this);
+ assertNoDocTypeNotationEntityAncestor(node, true);
+
+ boundaryUpdater(this, node, 0, node, getEndOffset(node));
+ },
+
+ selectNode: function(node) {
+ assertNotDetached(this);
+ assertNoDocTypeNotationEntityAncestor(node, false);
+ assertValidNodeType(node, beforeAfterNodeTypes);
+
+ var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
+ boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
+ },
+
+ compareBoundaryPoints: function(how, range) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertSameDocumentOrFragment(this.startContainer, range.startContainer);
+
+ var nodeA, offsetA, nodeB, offsetB;
+ var prefixA = (how == e2s || how == s2s) ? "start" : "end";
+ var prefixB = (how == s2e || how == s2s) ? "start" : "end";
+ nodeA = this[prefixA + "Container"];
+ offsetA = this[prefixA + "Offset"];
+ nodeB = range[prefixB + "Container"];
+ offsetB = range[prefixB + "Offset"];
+ return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
+ },
+
+ insertNode: function(node) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertValidNodeType(node, insertableNodeTypes);
+ assertNodeNotReadOnly(this.startContainer);
+
+ if (dom.isAncestorOf(node, this.startContainer, true)) {
+ throw new DOMException("HIERARCHY_REQUEST_ERR");
+ }
+
+ // No check for whether the container of the start of the Range is of a type that does not allow
+ // children of the type of node: the browser's DOM implementation should do this for us when we attempt
+ // to add the node
+
+ var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
+ this.setStartBefore(firstNodeInserted);
+ },
+
+ cloneContents: function() {
+ assertNotDetached(this);
+ assertRangeValid(this);
+
+ var clone, frag;
+ if (this.collapsed) {
+ return getRangeDocument(this).createDocumentFragment();
+ } else {
+ if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
+ clone = this.startContainer.cloneNode(true);
+ clone.data = clone.data.slice(this.startOffset, this.endOffset);
+ frag = getRangeDocument(this).createDocumentFragment();
+ frag.appendChild(clone);
+ return frag;
+ } else {
+ var iterator = new RangeIterator(this, true);
+ clone = cloneSubtree(iterator);
+ iterator.detach();
+ }
+ return clone;
+ }
+ },
+
+ extractContents: createRangeContentRemover(extractSubtree),
+
+ deleteContents: createRangeContentRemover(deleteSubtree),
+
+ canSurroundContents: function() {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertNodeNotReadOnly(this.startContainer);
+ assertNodeNotReadOnly(this.endContainer);
+
+ // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+ // no non-text nodes.
+ var iterator = new RangeIterator(this, true);
+ var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
+ (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+ iterator.detach();
+ return !boundariesInvalid;
+ },
+
+ surroundContents: function(node) {
+ assertValidNodeType(node, surroundNodeTypes);
+
+ if (!this.canSurroundContents()) {
+ throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
+ }
+
+ // Extract the contents
+ var content = this.extractContents();
+
+ // Clear the children of the node
+ if (node.hasChildNodes()) {
+ while (node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ }
+
+ // Insert the new node and add the extracted contents
+ insertNodeAtPosition(node, this.startContainer, this.startOffset);
+ node.appendChild(content);
+
+ this.selectNode(node);
+ },
+
+ cloneRange: function() {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ var range = new Range(getRangeDocument(this));
+ var i = rangeProperties.length, prop;
+ while (i--) {
+ prop = rangeProperties[i];
+ range[prop] = this[prop];
+ }
+ return range;
+ },
+
+ detach: function() {
+ detacher(this);
+ },
+
+ toString: function() {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ var sc = this.startContainer;
+ if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
+ return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
+ } else {
+ var textBits = [], iterator = new RangeIterator(this, true);
+
+ iterateSubtree(iterator, function(node) {
+ // Accept only text or CDATA nodes, not comments
+
+ if (node.nodeType == 3 || node.nodeType == 4) {
+ textBits.push(node.data);
+ }
+ });
+ iterator.detach();
+ return textBits.join("");
+ }
+ },
+
+ // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
+ // been removed from Mozilla.
+
+ compareNode: function(node) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+
+ var parent = node.parentNode;
+ var nodeIndex = dom.getNodeIndex(node);
+
+ if (!parent) {
+ throw new DOMException("NOT_FOUND_ERR");
+ }
+
+ var startComparison = this.comparePoint(parent, nodeIndex),
+ endComparison = this.comparePoint(parent, nodeIndex + 1);
+
+ if (startComparison < 0) { // Node starts before
+ return (endComparison > 0) ? n_b_a : n_b;
+ } else {
+ return (endComparison > 0) ? n_a : n_i;
+ }
+ },
+
+ comparePoint: function(node, offset) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertNode(node, "HIERARCHY_REQUEST_ERR");
+ assertSameDocumentOrFragment(node, this.startContainer);
+
+ if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
+ return -1;
+ } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
+ return 1;
+ }
+ return 0;
+ },
+
+ createContextualFragment: function(html) {
+ assertNotDetached(this);
+ var doc = getRangeDocument(this);
+ var container = doc.createElement("div");
+
+ // The next line is obviously non-standard but will work in all recent browsers
+ container.innerHTML = html;
+
+ var frag = doc.createDocumentFragment(), n;
+
+ while ( (n = container.firstChild) ) {
+ frag.appendChild(n);
+ }
+
+ return frag;
+ },
+
+ // This follows the Mozilla model whereby a node that borders a range is not considered to intersect with it
+ intersectsNode: function(node, touchingIsIntersecting) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertNode(node, "NOT_FOUND_ERR");
+ if (dom.getDocument(node) !== getRangeDocument(this)) {
+ return false;
+ }
+
+ var parent = node.parentNode, offset = dom.getNodeIndex(node);
+ assertNode(parent, "NOT_FOUND_ERR");
+
+ var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
+ endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
+
+ return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
+ },
+
+ isPointInRange: function(node, offset) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ assertNode(node, "HIERARCHY_REQUEST_ERR");
+ assertSameDocumentOrFragment(node, this.startContainer);
+
+ return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
+ (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
+ },
+
+ // The methods below are non-standard and invented by me.
+
+ // Sharing a boundary start-to-end or end-to-start does not count as intersection.
+ intersectsRange: function(range) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+
+ if (getRangeDocument(range) != getRangeDocument(this)) {
+ throw new DOMException("WRONG_DOCUMENT_ERR");
+ }
+
+ return dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset) < 0 &&
+ dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset) > 0;
+ },
+
+ intersection: function(range) {
+ if (this.intersectsRange(range)) {
+ var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
+ endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
+
+ var intersectionRange = this.cloneRange();
+
+ if (startComparison == -1) {
+ intersectionRange.setStart(range.startContainer, range.startOffset);
+ }
+ if (endComparison == 1) {
+ intersectionRange.setEnd(range.endContainer, range.endOffset);
+ }
+ return intersectionRange;
+ }
+ return null;
+ },
+
+ containsNode: function(node, allowPartial) {
+ if (allowPartial) {
+ return this.intersectsNode(node, false);
+ } else {
+ return this.compareNode(node) == n_i;
+ }
+ },
+
+ containsNodeContents: function(node) {
+ return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getEndOffset(node)) <= 0;
+ },
+
+ splitBoundaries: function() {
+ assertRangeValid(this);
+
+
+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+ var startEndSame = (sc === ec);
+
+ if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
+ dom.splitDataNode(ec, eo);
+
+ }
+
+ if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
+
+ sc = dom.splitDataNode(sc, so);
+ if (startEndSame) {
+ eo -= so;
+ ec = sc;
+ } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
+ eo++;
+ }
+ so = 0;
+
+ }
+ boundaryUpdater(this, sc, so, ec, eo);
+ },
+
+ normalizeBoundaries: function() {
+ assertRangeValid(this);
+
+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+
+ var mergeForward = function(node) {
+ var sibling = node.nextSibling;
+ if (sibling && sibling.nodeType == node.nodeType) {
+ ec = node;
+ eo = node.length;
+ node.appendData(sibling.data);
+ sibling.parentNode.removeChild(sibling);
+ }
+ };
+
+ var mergeBackward = function(node) {
+ var sibling = node.previousSibling;
+ if (sibling && sibling.nodeType == node.nodeType) {
+ sc = node;
+ var nodeLength = node.length;
+ so = sibling.length;
+ node.insertData(0, sibling.data);
+ sibling.parentNode.removeChild(sibling);
+ if (sc == ec) {
+ eo += so;
+ ec = sc;
+ } else if (ec == node.parentNode) {
+ var nodeIndex = dom.getNodeIndex(node);
+ if (eo == nodeIndex) {
+ ec = node;
+ eo = nodeLength;
+ } else if (eo > nodeIndex) {
+ eo--;
+ }
+ }
+ }
+ };
+
+ var normalizeStart = true;
+
+ if (dom.isCharacterDataNode(ec)) {
+ if (ec.length == eo) {
+ mergeForward(ec);
+ }
+ } else {
+ if (eo > 0) {
+ var endNode = ec.childNodes[eo - 1];
+ if (endNode && dom.isCharacterDataNode(endNode)) {
+ mergeForward(endNode);
+ }
+ }
+ normalizeStart = !this.collapsed;
+ }
+
+ if (normalizeStart) {
+ if (dom.isCharacterDataNode(sc)) {
+ if (so == 0) {
+ mergeBackward(sc);
+ }
+ } else {
+ if (so < sc.childNodes.length) {
+ var startNode = sc.childNodes[so];
+ if (startNode && dom.isCharacterDataNode(startNode)) {
+ mergeBackward(startNode);
+ }
+ }
+ }
+ } else {
+ sc = ec;
+ so = eo;
+ }
+
+ boundaryUpdater(this, sc, so, ec, eo);
+ },
+
+ createNodeIterator: function(nodeTypes, filter) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ return new RangeNodeIterator(this, nodeTypes, filter);
+ },
+
+ getNodes: function(nodeTypes, filter) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+ return getNodesInRange(this, nodeTypes, filter);
+ },
+
+ collapseToPoint: function(node, offset) {
+ assertNotDetached(this);
+ assertRangeValid(this);
+
+ assertNoDocTypeNotationEntityAncestor(node, true);
+ assertValidOffset(node, offset);
+
+ setRangeStartAndEnd(this, node, offset);
+ },
+
+ collapseBefore: function(node) {
+ assertNotDetached(this);
+
+ this.setEndBefore(node);
+ this.collapse(false);
+ },
+
+ collapseAfter: function(node) {
+ assertNotDetached(this);
+
+ this.setStartAfter(node);
+ this.collapse(true);
+ },
+
+ getName: function() {
+ return "DomRange";
+ },
+
+ equals: function(range) {
+ return Range.rangesEqual(this, range);
+ },
+
+ inspect: function() {
+ return inspect(this);
+ }
+ };
+
+ copyComparisonConstants(constructor);
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ // Updates commonAncestorContainer and collapsed after boundary change
+ function updateCollapsedAndCommonAncestor(range) {
+ range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+ range.commonAncestorContainer = range.collapsed ?
+ range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
+ }
+
+ function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
+ var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
+ var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
+
+ range.startContainer = startContainer;
+ range.startOffset = startOffset;
+ range.endContainer = endContainer;
+ range.endOffset = endOffset;
+
+ updateCollapsedAndCommonAncestor(range);
+ dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
+ }
+
+ function detach(range) {
+ assertNotDetached(range);
+ range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
+ range.collapsed = range.commonAncestorContainer = null;
+ dispatchEvent(range, "detach", null);
+ range._listeners = null;
+ }
+
+ /**
+ * @constructor
+ */
+ function Range(doc) {
+ this.startContainer = doc;
+ this.startOffset = 0;
+ this.endContainer = doc;
+ this.endOffset = 0;
+ this._listeners = {
+ boundarychange: [],
+ detach: []
+ };
+ updateCollapsedAndCommonAncestor(this);
+ }
+
+ createPrototypeRange(Range, updateBoundaries, detach);
+
+ Range.fromRange = function(r) {
+ var range = new Range(getRangeDocument(r));
+ updateBoundaries(range, r.startContainer, r.startOffset, r.endContainer, r.endOffset);
+ return range;
+ };
+
+ Range.rangeProperties = rangeProperties;
+ Range.RangeIterator = RangeIterator;
+ Range.copyComparisonConstants = copyComparisonConstants;
+ Range.createPrototypeRange = createPrototypeRange;
+ Range.inspect = inspect;
+ Range.getRangeDocument = getRangeDocument;
+ Range.rangesEqual = function(r1, r2) {
+ return r1.startContainer === r2.startContainer &&
+ r1.startOffset === r2.startOffset &&
+ r1.endContainer === r2.endContainer &&
+ r1.endOffset === r2.endOffset;
+ };
+ Range.getEndOffset = getEndOffset;
+
+ api.DomRange = Range;
+ api.RangeException = RangeException;
+});
+rangy.createModule("WrappedRange", function(api, module) {
+ api.requireModules( ["DomUtil", "DomRange"] );
+
+ /**
+ * @constructor
+ */
+ var WrappedRange;
+ var dom = api.dom;
+ var DomPosition = dom.DomPosition;
+ var DomRange = api.DomRange;
+
+
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ /*
+ This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
+ method. For example, in the following (where pipes denote the selection boundaries):
+
+ <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
+
+ var range = document.selection.createRange();
+ alert(range.parentElement().id); // Should alert "ul" but alerts "b"
+
+ This method returns the common ancestor node of the following:
+ - the parentElement() of the textRange
+ - the parentElement() of the textRange after calling collapse(true)
+ - the parentElement() of the textRange after calling collapse(false)
+ */
+ function getTextRangeContainerElement(textRange) {
+ var parentEl = textRange.parentElement();
+
+ var range = textRange.duplicate();
+ range.collapse(true);
+ var startEl = range.parentElement();
+ range = textRange.duplicate();
+ range.collapse(false);
+ var endEl = range.parentElement();
+ var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
+
+ return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
+ }
+
+ function textRangeIsCollapsed(textRange) {
+ return textRange.compareEndPoints("StartToEnd", textRange) == 0;
+ }
+
+ // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
+ // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
+ // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
+ // for inputs and images, plus optimizations.
+ function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
+ var workingRange = textRange.duplicate();
+
+ workingRange.collapse(isStart);
+ var containerElement = workingRange.parentElement();
+
+ // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
+ // check for that
+ // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
+ if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
+ containerElement = wholeRangeContainerElement;
+
+ }
+
+
+
+ // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
+ // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
+ if (!containerElement.canHaveHTML) {
+ return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
+ }
+
+ var workingNode = dom.getDocument(containerElement).createElement("span");
+ var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
+ var previousNode, nextNode, boundaryPosition, boundaryNode;
+
+ // Move the working range through the container's children, starting at the end and working backwards, until the
+ // working range reaches or goes past the boundary we're interested in
+ do {
+ containerElement.insertBefore(workingNode, workingNode.previousSibling);
+ workingRange.moveToElementText(workingNode);
+ } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
+ workingNode.previousSibling);
+
+ // We've now reached or gone past the boundary of the text range we're interested in
+ // so have identified the node we want
+ boundaryNode = workingNode.nextSibling;
+
+ if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
+ // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
+ // node containing the text range's boundary, so we move the end of the working range to the boundary point
+ // and measure the length of its text to get the boundary's offset within the node.
+ workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
+
+
+ var offset;
+
+ if (/[\r\n]/.test(boundaryNode.data)) {
+ /*
+ For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
+ for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
+
+ - Each line break is represented as \r in the text node's data/nodeValue properties
+ - Each line break is represented as \r\n in the TextRange's 'text' property
+ - The 'text' property of the TextRange does not contain trailing line breaks
+
+ To get round the problem presented by the final fact above, we can use the fact that TextRange's
+ moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
+ the same as the number of characters it was instructed to move. The simplest approach is to use this to
+ store the characters moved when moving both the start and end of the range to the start of the document
+ body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
+ However, this is extremely slow when the document is large and the range is near the end of it. Clearly
+ doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
+ problem.
+
+ Another approach that works is to use moveStart() to move the start boundary of the range up to the end
+ boundary one character at a time and incrementing a counter with the value returned by the moveStart()
+ call. However, the check for whether the start boundary has reached the end boundary is expensive, so
+ this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
+ the range within the document).
+
+ The method below is a hybrid of the two methods above. It uses the fact that a string containing the
+ TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
+ text of the TextRange, so the start of the range is moved that length initially and then a character at
+ a time to make up for any trailing line breaks not contained in the 'text' property. This has good
+ performance in most situations compared to the previous two methods.
+ */
+ var tempRange = workingRange.duplicate();
+ var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+ offset = tempRange.moveStart("character", rangeLength);
+ while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+ offset++;
+ tempRange.moveStart("character", 1);
+ }
+ } else {
+ offset = workingRange.text.length;
+ }
+ boundaryPosition = new DomPosition(boundaryNode, offset);
+ } else {
+
+
+ // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+ // a position within that, and likewise for a start boundary preceding a character data node
+ previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+ nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+
+
+
+ if (nextNode && dom.isCharacterDataNode(nextNode)) {
+ boundaryPosition = new DomPosition(nextNode, 0);
+ } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
+ boundaryPosition = new DomPosition(previousNode, previousNode.length);
+ } else {
+ boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+ }
+ }
+
+ // Clean up
+ workingNode.parentNode.removeChild(workingNode);
+
+ return boundaryPosition;
+ }
+
+ // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
+ // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+ // (http://code.google.com/p/ierange/)
+ function createBoundaryTextRange(boundaryPosition, isStart) {
+ var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+ var doc = dom.getDocument(boundaryPosition.node);
+ var workingNode, childNodes, workingRange = doc.body.createTextRange();
+ var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
+
+ if (nodeIsDataNode) {
+ boundaryNode = boundaryPosition.node;
+ boundaryParent = boundaryNode.parentNode;
+ } else {
+ childNodes = boundaryPosition.node.childNodes;
+ boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+ boundaryParent = boundaryPosition.node;
+ }
+
+ // Position the range immediately before the node containing the boundary
+ workingNode = doc.createElement("span");
+
+ // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
+ // element rather than immediately before or after it, which is what we want
+ workingNode.innerHTML = "&#feff;";
+
+ // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+ // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+ if (boundaryNode) {
+ boundaryParent.insertBefore(workingNode, boundaryNode);
+ } else {
+ boundaryParent.appendChild(workingNode);
+ }
+
+ workingRange.moveToElementText(workingNode);
+ workingRange.collapse(!isStart);
+
+ // Clean up
+ boundaryParent.removeChild(workingNode);
+
+ // Move the working range to the text offset, if required
+ if (nodeIsDataNode) {
+ workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+ }
+
+ return workingRange;
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ if (api.features.implementsDomRange) {
+ // This is a wrapper around the browser's native DOM Range. It has two aims:
+ // - Provide workarounds for specific browser bugs
+ // - provide convenient extensions, as found in Rangy's DomRange
+
+ (function() {
+ var rangeProto;
+ var rangeProperties = DomRange.rangeProperties;
+ var canSetRangeStartAfterEnd;
+
+ function updateRangeProperties(range) {
+ var i = rangeProperties.length, prop;
+ while (i--) {
+ prop = rangeProperties[i];
+ range[prop] = range.nativeRange[prop];
+ }
+ }
+
+ function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
+ var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+ var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
+
+ // Always set both boundaries for the benefit of IE9 (see issue 35)
+ if (startMoved || endMoved) {
+ range.setEnd(endContainer, endOffset);
+ range.setStart(startContainer, startOffset);
+ }
+ }
+
+ function detach(range) {
+ range.nativeRange.detach();
+ range.detached = true;
+ var i = rangeProperties.length, prop;
+ while (i--) {
+ prop = rangeProperties[i];
+ range[prop] = null;
+ }
+ }
+
+ var createBeforeAfterNodeSetter;
+
+ WrappedRange = function(range) {
+ if (!range) {
+ throw new Error("Range must be specified");
+ }
+ this.nativeRange = range;
+ updateRangeProperties(this);
+ };
+
+ DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
+
+ rangeProto = WrappedRange.prototype;
+
+ rangeProto.selectNode = function(node) {
+ this.nativeRange.selectNode(node);
+ updateRangeProperties(this);
+ };
+
+ rangeProto.deleteContents = function() {
+ this.nativeRange.deleteContents();
+ updateRangeProperties(this);
+ };
+
+ rangeProto.extractContents = function() {
+ var frag = this.nativeRange.extractContents();
+ updateRangeProperties(this);
+ return frag;
+ };
+
+ rangeProto.cloneContents = function() {
+ return this.nativeRange.cloneContents();
+ };
+
+ // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
+ // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
+ // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
+ // insertNode, which works but is almost certainly slower than the native implementation.
+/*
+ rangeProto.insertNode = function(node) {
+ this.nativeRange.insertNode(node);
+ updateRangeProperties(this);
+ };
+*/
+
+ rangeProto.surroundContents = function(node) {
+ this.nativeRange.surroundContents(node);
+ updateRangeProperties(this);
+ };
+
+ rangeProto.collapse = function(isStart) {
+ this.nativeRange.collapse(isStart);
+ updateRangeProperties(this);
+ };
+
+ rangeProto.cloneRange = function() {
+ return new WrappedRange(this.nativeRange.cloneRange());
+ };
+
+ rangeProto.refresh = function() {
+ updateRangeProperties(this);
+ };
+
+ rangeProto.toString = function() {
+ return this.nativeRange.toString();
+ };
+
+ // Create test range and node for feature detection
+
+ var testTextNode = document.createTextNode("test");
+ dom.getBody(document).appendChild(testTextNode);
+ var range = document.createRange();
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
+ // correct for it
+
+ range.setStart(testTextNode, 0);
+ range.setEnd(testTextNode, 0);
+
+ try {
+ range.setStart(testTextNode, 1);
+ canSetRangeStartAfterEnd = true;
+
+ rangeProto.setStart = function(node, offset) {
+ this.nativeRange.setStart(node, offset);
+ updateRangeProperties(this);
+ };
+
+ rangeProto.setEnd = function(node, offset) {
+ this.nativeRange.setEnd(node, offset);
+ updateRangeProperties(this);
+ };
+
+ createBeforeAfterNodeSetter = function(name) {
+ return function(node) {
+ this.nativeRange[name](node);
+ updateRangeProperties(this);
+ };
+ };
+
+ } catch(ex) {
+
+
+ canSetRangeStartAfterEnd = false;
+
+ rangeProto.setStart = function(node, offset) {
+ try {
+ this.nativeRange.setStart(node, offset);
+ } catch (ex) {
+ this.nativeRange.setEnd(node, offset);
+ this.nativeRange.setStart(node, offset);
+ }
+ updateRangeProperties(this);
+ };
+
+ rangeProto.setEnd = function(node, offset) {
+ try {
+ this.nativeRange.setEnd(node, offset);
+ } catch (ex) {
+ this.nativeRange.setStart(node, offset);
+ this.nativeRange.setEnd(node, offset);
+ }
+ updateRangeProperties(this);
+ };
+
+ createBeforeAfterNodeSetter = function(name, oppositeName) {
+ return function(node) {
+ try {
+ this.nativeRange[name](node);
+ } catch (ex) {
+ this.nativeRange[oppositeName](node);
+ this.nativeRange[name](node);
+ }
+ updateRangeProperties(this);
+ };
+ };
+ }
+
+ rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
+ rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
+ rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
+ rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
+ // the 0th character of the text node
+ range.selectNodeContents(testTextNode);
+ if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
+ range.startOffset == 0 && range.endOffset == testTextNode.length) {
+ rangeProto.selectNodeContents = function(node) {
+ this.nativeRange.selectNodeContents(node);
+ updateRangeProperties(this);
+ };
+ } else {
+ rangeProto.selectNodeContents = function(node) {
+ this.setStart(node, 0);
+ this.setEnd(node, DomRange.getEndOffset(node));
+ };
+ }
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
+ // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
+
+ range.selectNodeContents(testTextNode);
+ range.setEnd(testTextNode, 3);
+
+ var range2 = document.createRange();
+ range2.selectNodeContents(testTextNode);
+ range2.setEnd(testTextNode, 4);
+ range2.setStart(testTextNode, 2);
+
+ if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
+ range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
+ // This is the wrong way round, so correct for it
+
+
+ rangeProto.compareBoundaryPoints = function(type, range) {
+ range = range.nativeRange || range;
+ if (type == range.START_TO_END) {
+ type = range.END_TO_START;
+ } else if (type == range.END_TO_START) {
+ type = range.START_TO_END;
+ }
+ return this.nativeRange.compareBoundaryPoints(type, range);
+ };
+ } else {
+ rangeProto.compareBoundaryPoints = function(type, range) {
+ return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
+ };
+ }
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Clean up
+ dom.getBody(document).removeChild(testTextNode);
+ range.detach();
+ range2.detach();
+ })();
+
+ } else if (api.features.implementsTextRange) {
+ // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+ // prototype
+
+ WrappedRange = function(textRange) {
+ this.textRange = textRange;
+ this.refresh();
+ };
+
+ WrappedRange.prototype = new DomRange(document);
+
+ WrappedRange.prototype.refresh = function() {
+ var start, end;
+
+ // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+ var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+ if (textRangeIsCollapsed(this.textRange)) {
+ end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
+ } else {
+
+ start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+ end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
+ }
+
+ this.setStart(start.node, start.offset);
+ this.setEnd(end.node, end.offset);
+ };
+
+ WrappedRange.rangeToTextRange = function(range) {
+ if (range.collapsed) {
+ return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+ } else {
+ var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+ var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+ var textRange = dom.getDocument(range.startContainer).body.createTextRange();
+ textRange.setEndPoint("StartToStart", startRange);
+ textRange.setEndPoint("EndToEnd", endRange);
+ return textRange;
+ }
+ };
+
+ DomRange.copyComparisonConstants(WrappedRange);
+
+ // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+ var globalObj = (function() { return this; })();
+ if (typeof globalObj.Range == "undefined") {
+ globalObj.Range = WrappedRange;
+ }
+ }
+
+ WrappedRange.prototype.getName = function() {
+ return "WrappedRange";
+ };
+
+ api.WrappedRange = WrappedRange;
+
+ api.createNativeRange = function(doc) {
+ doc = doc || document;
+ if (api.features.implementsDomRange) {
+ return doc.createRange();
+ } else if (api.features.implementsTextRange) {
+ return doc.body.createTextRange();
+ }
+ };
+
+ api.createRange = function(doc) {
+ doc = doc || document;
+ return new WrappedRange(api.createNativeRange(doc));
+ };
+
+ api.createRangyRange = function(doc) {
+ doc = doc || document;
+ return new DomRange(doc);
+ };
+
+ api.createIframeRange = function(iframeEl) {
+ return api.createRange(dom.getIframeDocument(iframeEl));
+ };
+
+ api.createIframeRangyRange = function(iframeEl) {
+ return api.createRangyRange(dom.getIframeDocument(iframeEl));
+ };
+
+ api.addCreateMissingNativeApiListener(function(win) {
+ var doc = win.document;
+ if (typeof doc.createRange == "undefined") {
+ doc.createRange = function() {
+ return api.createRange(this);
+ };
+ }
+ doc = win = null;
+ });
+});
+rangy.createModule("WrappedSelection", function(api, module) {
+ // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
+ // spec (http://html5.org/specs/dom-range.html)
+
+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+ api.config.checkSelectionRanges = true;
+
+ var BOOLEAN = "boolean",
+ windowPropertyName = "_rangySelection",
+ dom = api.dom,
+ util = api.util,
+ DomRange = api.DomRange,
+ WrappedRange = api.WrappedRange,
+ DOMException = api.DOMException,
+ DomPosition = dom.DomPosition,
+ getSelection,
+ selectionIsCollapsed,
+ CONTROL = "Control";
+
+
+
+ function getWinSelection(winParam) {
+ return (winParam || window).getSelection();
+ }
+
+ function getDocSelection(winParam) {
+ return (winParam || window).document.selection;
+ }
+
+ // Test for the Range/TextRange and Selection features required
+ // Test for ability to retrieve selection
+ var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
+ implementsDocSelection = api.util.isHostObject(document, "selection");
+
+ if (implementsWinGetSelection) {
+ getSelection = getWinSelection;
+ api.isSelectionValid = function() {
+ return true;
+ };
+ } else if (implementsDocSelection) {
+ getSelection = getDocSelection;
+ api.isSelectionValid = function(winParam) {
+ var doc = (winParam || window).document, nativeSel = doc.selection;
+
+ // Check whether the selection TextRange is actually contained within the correct document
+ return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
+ };
+ } else {
+ module.fail("No means of obtaining a selection object");
+ }
+
+ api.getNativeSelection = getSelection;
+
+ var testSelection = getSelection();
+ var testRange = api.createNativeRange(document);
+ var body = dom.getBody(document);
+
+ // Obtaining a range from a selection
+ var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
+ util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
+ api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+ // Test for existence of native selection extend() method
+ var selectionHasExtend = util.isHostMethod(testSelection, "extend");
+ api.features.selectionHasExtend = selectionHasExtend;
+
+ // Test if rangeCount exists
+ var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
+ api.features.selectionHasRangeCount = selectionHasRangeCount;
+
+ var selectionSupportsMultipleRanges = false;
+ var collapsedNonEditableSelectionsSupported = true;
+
+ if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+ typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
+
+ (function() {
+ var iframe = document.createElement("iframe");
+ body.appendChild(iframe);
+
+ var iframeDoc = dom.getIframeDocument(iframe);
+ iframeDoc.open();
+ iframeDoc.write("<html><head></head><body>12</body></html>");
+ iframeDoc.close();
+
+ var sel = dom.getIframeWindow(iframe).getSelection();
+ var docEl = iframeDoc.documentElement;
+ var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
+
+ // Test whether the native selection will allow a collapsed selection within a non-editable element
+ var r1 = iframeDoc.createRange();
+ r1.setStart(textNode, 1);
+ r1.collapse(true);
+ sel.addRange(r1);
+ collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+ sel.removeAllRanges();
+
+ // Test whether the native selection is capable of supporting multiple ranges
+ var r2 = r1.cloneRange();
+ r1.setStart(textNode, 0);
+ r2.setEnd(textNode, 2);
+ sel.addRange(r1);
+ sel.addRange(r2);
+
+ selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+
+ // Clean up
+ r1.detach();
+ r2.detach();
+
+ body.removeChild(iframe);
+ })();
+ }
+
+ api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+ api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+ // ControlRanges
+ var implementsControlRange = false, testControlRange;
+
+ if (body && util.isHostMethod(body, "createControlRange")) {
+ testControlRange = body.createControlRange();
+ if (util.areHostProperties(testControlRange, ["item", "add"])) {
+ implementsControlRange = true;
+ }
+ }
+ api.features.implementsControlRange = implementsControlRange;
+
+ // Selection collapsedness
+ if (selectionHasAnchorAndFocus) {
+ selectionIsCollapsed = function(sel) {
+ return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+ };
+ } else {
+ selectionIsCollapsed = function(sel) {
+ return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+ };
+ }
+
+ function updateAnchorAndFocusFromRange(sel, range, backwards) {
+ var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
+ sel.anchorNode = range[anchorPrefix + "Container"];
+ sel.anchorOffset = range[anchorPrefix + "Offset"];
+ sel.focusNode = range[focusPrefix + "Container"];
+ sel.focusOffset = range[focusPrefix + "Offset"];
+ }
+
+ function updateAnchorAndFocusFromNativeSelection(sel) {
+ var nativeSel = sel.nativeSelection;
+ sel.anchorNode = nativeSel.anchorNode;
+ sel.anchorOffset = nativeSel.anchorOffset;
+ sel.focusNode = nativeSel.focusNode;
+ sel.focusOffset = nativeSel.focusOffset;
+ }
+
+ function updateEmptySelection(sel) {
+ sel.anchorNode = sel.focusNode = null;
+ sel.anchorOffset = sel.focusOffset = 0;
+ sel.rangeCount = 0;
+ sel.isCollapsed = true;
+ sel._ranges.length = 0;
+ }
+
+ function getNativeRange(range) {
+ var nativeRange;
+ if (range instanceof DomRange) {
+ nativeRange = range._selectionNativeRange;
+ if (!nativeRange) {
+ nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
+ nativeRange.setEnd(range.endContainer, range.endOffset);
+ nativeRange.setStart(range.startContainer, range.startOffset);
+ range._selectionNativeRange = nativeRange;
+ range.attachListener("detach", function() {
+
+ this._selectionNativeRange = null;
+ });
+ }
+ } else if (range instanceof WrappedRange) {
+ nativeRange = range.nativeRange;
+ } else if (window.Range && (range instanceof Range)) {
+ nativeRange = range;
+ }
+ return nativeRange;
+ }
+
+ function rangeContainsSingleElement(rangeNodes) {
+ if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+ return false;
+ }
+ for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+ if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function getSingleElementFromRange(range) {
+ var nodes = range.getNodes();
+ if (!rangeContainsSingleElement(nodes)) {
+ throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+ }
+ return nodes[0];
+ }
+
+ function isTextRange(range) {
+ return !!range && typeof range.text != "undefined";
+ }
+
+ function updateFromTextRange(sel, range) {
+ // Create a Range from the selected TextRange
+ var wrappedRange = new WrappedRange(range);
+ sel._ranges = [wrappedRange];
+
+ updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+ sel.rangeCount = 1;
+ sel.isCollapsed = wrappedRange.collapsed;
+ }
+
+ function updateControlSelection(sel) {
+ // Update the wrapped selection based on what's now in the native selection
+ sel._ranges.length = 0;
+ if (sel.docSelection.type == "None") {
+ updateEmptySelection(sel);
+ } else {
+ var controlRange = sel.docSelection.createRange();
+ if (isTextRange(controlRange)) {
+ // This case (where the selection type is "Control" and calling createRange() on the selection returns
+ // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+ // ControlRange have been removed from the ControlRange and removed from the document.
+ updateFromTextRange(sel, controlRange);
+ } else {
+ sel.rangeCount = controlRange.length;
+ var range, doc = dom.getDocument(controlRange.item(0));
+ for (var i = 0; i < sel.rangeCount; ++i) {
+ range = api.createRange(doc);
+ range.selectNode(controlRange.item(i));
+ sel._ranges.push(range);
+ }
+ sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+ }
+ }
+ }
+
+ function addRangeToControlSelection(sel, range) {
+ var controlRange = sel.docSelection.createRange();
+ var rangeElement = getSingleElementFromRange(range);
+
+ // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+ // contained by the supplied range
+ var doc = dom.getDocument(controlRange.item(0));
+ var newControlRange = dom.getBody(doc).createControlRange();
+ for (var i = 0, len = controlRange.length; i < len; ++i) {
+ newControlRange.add(controlRange.item(i));
+ }
+ try {
+ newControlRange.add(rangeElement);
+ } catch (ex) {
+ throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+ }
+ newControlRange.select();
+
+ // Update the wrapped selection based on what's now in the native selection
+ updateControlSelection(sel);
+ }
+
+ var getSelectionRangeAt;
+
+ if (util.isHostMethod(testSelection, "getRangeAt")) {
+ getSelectionRangeAt = function(sel, index) {
+ try {
+ return sel.getRangeAt(index);
+ } catch(ex) {
+ return null;
+ }
+ };
+ } else if (selectionHasAnchorAndFocus) {
+ getSelectionRangeAt = function(sel) {
+ var doc = dom.getDocument(sel.anchorNode);
+ var range = api.createRange(doc);
+ range.setStart(sel.anchorNode, sel.anchorOffset);
+ range.setEnd(sel.focusNode, sel.focusOffset);
+
+ // Handle the case when the selection was selected backwards (from the end to the start in the
+ // document)
+ if (range.collapsed !== this.isCollapsed) {
+ range.setStart(sel.focusNode, sel.focusOffset);
+ range.setEnd(sel.anchorNode, sel.anchorOffset);
+ }
+
+ return range;
+ };
+ }
+
+ /**
+ * @constructor
+ */
+ function WrappedSelection(selection, docSelection, win) {
+ this.nativeSelection = selection;
+ this.docSelection = docSelection;
+ this._ranges = [];
+ this.win = win;
+ this.refresh();
+ }
+
+ api.getSelection = function(win) {
+ win = win || window;
+ var sel = win[windowPropertyName];
+ var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+ if (sel) {
+ sel.nativeSelection = nativeSel;
+ sel.docSelection = docSel;
+ sel.refresh(win);
+ } else {
+ sel = new WrappedSelection(nativeSel, docSel, win);
+ win[windowPropertyName] = sel;
+ }
+ return sel;
+ };
+
+ api.getIframeSelection = function(iframeEl) {
+ return api.getSelection(dom.getIframeWindow(iframeEl));
+ };
+
+ var selProto = WrappedSelection.prototype;
+
+ function createControlSelection(sel, ranges) {
+ // Ensure that the selection becomes of type "Control"
+ var doc = dom.getDocument(ranges[0].startContainer);
+ var controlRange = dom.getBody(doc).createControlRange();
+ for (var i = 0, el; i < rangeCount; ++i) {
+ el = getSingleElementFromRange(ranges[i]);
+ try {
+ controlRange.add(el);
+ } catch (ex) {
+ throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
+ }
+ }
+ controlRange.select();
+
+ // Update the wrapped selection based on what's now in the native selection
+ updateControlSelection(sel);
+ }
+
+ // Selecting a range
+ if (selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+ selProto.removeAllRanges = function() {
+ this.nativeSelection.removeAllRanges();
+ updateEmptySelection(this);
+ };
+
+ var addRangeBackwards = function(sel, range) {
+ var doc = DomRange.getRangeDocument(range);
+ var endRange = api.createRange(doc);
+ endRange.collapseToPoint(range.endContainer, range.endOffset);
+ sel.nativeSelection.addRange(getNativeRange(endRange));
+ sel.nativeSelection.extend(range.startContainer, range.startOffset);
+ sel.refresh();
+ };
+
+ if (selectionHasRangeCount) {
+ selProto.addRange = function(range, backwards) {
+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+ addRangeToControlSelection(this, range);
+ } else {
+ if (backwards && selectionHasExtend) {
+ addRangeBackwards(this, range);
+ } else {
+ var previousRangeCount;
+ if (selectionSupportsMultipleRanges) {
+ previousRangeCount = this.rangeCount;
+ } else {
+ this.removeAllRanges();
+ previousRangeCount = 0;
+ }
+ this.nativeSelection.addRange(getNativeRange(range));
+
+ // Check whether adding the range was successful
+ this.rangeCount = this.nativeSelection.rangeCount;
+
+ if (this.rangeCount == previousRangeCount + 1) {
+ // The range was added successfully
+
+ // Check whether the range that we added to the selection is reflected in the last range extracted from
+ // the selection
+ if (api.config.checkSelectionRanges) {
+ var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+ if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
+ // Happens in WebKit with, for example, a selection placed at the start of a text node
+ range = new WrappedRange(nativeRange);
+ }
+ }
+ this._ranges[this.rangeCount - 1] = range;
+ updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
+ this.isCollapsed = selectionIsCollapsed(this);
+ } else {
+ // The range was not added successfully. The simplest thing is to refresh
+ this.refresh();
+ }
+ }
+ }
+ };
+ } else {
+ selProto.addRange = function(range, backwards) {
+ if (backwards && selectionHasExtend) {
+ addRangeBackwards(this, range);
+ } else {
+ this.nativeSelection.addRange(getNativeRange(range));
+ this.refresh();
+ }
+ };
+ }
+
+ selProto.setRanges = function(ranges) {
+ if (implementsControlRange && ranges.length > 1) {
+ createControlSelection(this, ranges);
+ } else {
+ this.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ this.addRange(ranges[i]);
+ }
+ }
+ };
+ } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
+ implementsControlRange && implementsDocSelection) {
+
+ selProto.removeAllRanges = function() {
+ // Added try/catch as fix for issue #21
+ try {
+ this.docSelection.empty();
+
+ // Check for empty() not working (issue #24)
+ if (this.docSelection.type != "None") {
+ // Work around failure to empty a control selection by instead selecting a TextRange and then
+ // calling empty()
+ var doc;
+ if (this.anchorNode) {
+ doc = dom.getDocument(this.anchorNode);
+ } else if (this.docSelection.type == CONTROL) {
+ var controlRange = this.docSelection.createRange();
+ if (controlRange.length) {
+ doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
+ }
+ }
+ if (doc) {
+ var textRange = doc.body.createTextRange();
+ textRange.select();
+ this.docSelection.empty();
+ }
+ }
+ } catch(ex) {}
+ updateEmptySelection(this);
+ };
+
+ selProto.addRange = function(range) {
+ if (this.docSelection.type == CONTROL) {
+ addRangeToControlSelection(this, range);
+ } else {
+ WrappedRange.rangeToTextRange(range).select();
+ this._ranges[0] = range;
+ this.rangeCount = 1;
+ this.isCollapsed = this._ranges[0].collapsed;
+ updateAnchorAndFocusFromRange(this, range, false);
+ }
+ };
+
+ selProto.setRanges = function(ranges) {
+ this.removeAllRanges();
+ var rangeCount = ranges.length;
+ if (rangeCount > 1) {
+ createControlSelection(this, ranges);
+ } else if (rangeCount) {
+ this.addRange(ranges[0]);
+ }
+ };
+ } else {
+ module.fail("No means of selecting a Range or TextRange was found");
+ return false;
+ }
+
+ selProto.getRangeAt = function(index) {
+ if (index < 0 || index >= this.rangeCount) {
+ throw new DOMException("INDEX_SIZE_ERR");
+ } else {
+ return this._ranges[index];
+ }
+ };
+
+ var refreshSelection;
+
+ if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
+ refreshSelection = function(sel) {
+ if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+ updateControlSelection(sel);
+ } else {
+ sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+ if (sel.rangeCount) {
+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+ sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+ }
+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
+ sel.isCollapsed = selectionIsCollapsed(sel);
+ } else {
+ updateEmptySelection(sel);
+ }
+ }
+ };
+ } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
+ refreshSelection = function(sel) {
+ var range, nativeSel = sel.nativeSelection;
+ if (nativeSel.anchorNode) {
+ range = getSelectionRangeAt(nativeSel, 0);
+ sel._ranges = [range];
+ sel.rangeCount = 1;
+ updateAnchorAndFocusFromNativeSelection(sel);
+ sel.isCollapsed = selectionIsCollapsed(sel);
+ } else {
+ updateEmptySelection(sel);
+ }
+ };
+ } else if (util.isHostMethod(testSelection, "createRange") && implementsDocSelection) {
+ refreshSelection = function(sel) {
+ var range;
+ if (api.isSelectionValid(sel.win)) {
+ range = sel.docSelection.createRange();
+ } else {
+ range = dom.getBody(sel.win.document).createTextRange();
+ range.collapse(true);
+ }
+
+
+ if (sel.docSelection.type == CONTROL) {
+ updateControlSelection(sel);
+ } else if (isTextRange(range)) {
+ updateFromTextRange(sel, range);
+ } else {
+ updateEmptySelection(sel);
+ }
+ };
+ } else {
+ module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+ return false;
+ }
+
+ selProto.refresh = function(checkForChanges) {
+ var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+ refreshSelection(this);
+ if (checkForChanges) {
+ var i = oldRanges.length;
+ if (i != this._ranges.length) {
+ return false;
+ }
+ while (i--) {
+ if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ };
+
+ // Removal of a single range
+ var removeRangeManually = function(sel, range) {
+ var ranges = sel.getAllRanges(), removed = false;
+ sel.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ if (removed || range !== ranges[i]) {
+ sel.addRange(ranges[i]);
+ } else {
+ // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
+ // times. removeRange should only remove the first instance, so the following ensures only the first
+ // instance is removed
+ removed = true;
+ }
+ }
+ if (!sel.rangeCount) {
+ updateEmptySelection(sel);
+ }
+ };
+
+ if (implementsControlRange) {
+ selProto.removeRange = function(range) {
+ if (this.docSelection.type == CONTROL) {
+ var controlRange = this.docSelection.createRange();
+ var rangeElement = getSingleElementFromRange(range);
+
+ // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+ // element contained by the supplied range
+ var doc = dom.getDocument(controlRange.item(0));
+ var newControlRange = dom.getBody(doc).createControlRange();
+ var el, removed = false;
+ for (var i = 0, len = controlRange.length; i < len; ++i) {
+ el = controlRange.item(i);
+ if (el !== rangeElement || removed) {
+ newControlRange.add(controlRange.item(i));
+ } else {
+ removed = true;
+ }
+ }
+ newControlRange.select();
+
+ // Update the wrapped selection based on what's now in the native selection
+ updateControlSelection(this);
+ } else {
+ removeRangeManually(this, range);
+ }
+ };
+ } else {
+ selProto.removeRange = function(range) {
+ removeRangeManually(this, range);
+ };
+ }
+
+ // Detecting if a selection is backwards
+ var selectionIsBackwards;
+ if (selectionHasAnchorAndFocus && api.features.implementsDomRange) {
+ selectionIsBackwards = function(sel) {
+ var backwards = false;
+ if (sel.anchorNode) {
+ backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+ }
+ return backwards;
+ };
+
+ selProto.isBackwards = function() {
+ return selectionIsBackwards(this);
+ };
+ } else {
+ selectionIsBackwards = selProto.isBackwards = function() {
+ return false;
+ };
+ }
+
+ // Selection text
+ // This is conformant to the HTML 5 draft spec but differs from WebKit and Mozilla's implementation
+ selProto.toString = function() {
+
+ var rangeTexts = [];
+ for (var i = 0, len = this.rangeCount; i < len; ++i) {
+ rangeTexts[i] = "" + this._ranges[i];
+ }
+ return rangeTexts.join("");
+ };
+
+ function assertNodeInSameDocument(sel, node) {
+ if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
+ throw new DOMException("WRONG_DOCUMENT_ERR");
+ }
+ }
+
+ // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
+ selProto.collapse = function(node, offset) {
+ assertNodeInSameDocument(this, node);
+ var range = api.createRange(dom.getDocument(node));
+ range.collapseToPoint(node, offset);
+ this.removeAllRanges();
+ this.addRange(range);
+ this.isCollapsed = true;
+ };
+
+ selProto.collapseToStart = function() {
+ if (this.rangeCount) {
+ var range = this._ranges[0];
+ this.collapse(range.startContainer, range.startOffset);
+ } else {
+ throw new DOMException("INVALID_STATE_ERR");
+ }
+ };
+
+ selProto.collapseToEnd = function() {
+ if (this.rangeCount) {
+ var range = this._ranges[this.rangeCount - 1];
+ this.collapse(range.endContainer, range.endOffset);
+ } else {
+ throw new DOMException("INVALID_STATE_ERR");
+ }
+ };
+
+ // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
+ // never used by Rangy.
+ selProto.selectAllChildren = function(node) {
+ assertNodeInSameDocument(this, node);
+ var range = api.createRange(dom.getDocument(node));
+ range.selectNodeContents(node);
+ this.removeAllRanges();
+ this.addRange(range);
+ };
+
+ selProto.deleteFromDocument = function() {
+ // Sepcial behaviour required for Control selections
+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+ var controlRange = this.docSelection.createRange();
+ var element;
+ while (controlRange.length) {
+ element = controlRange.item(0);
+ controlRange.remove(element);
+ element.parentNode.removeChild(element);
+ }
+ this.refresh();
+ } else if (this.rangeCount) {
+ var ranges = this.getAllRanges();
+ this.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ ranges[i].deleteContents();
+ }
+ // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
+ // range. Firefox moves the selection to where the final selected range was, so we emulate that
+ this.addRange(ranges[len - 1]);
+ }
+ };
+
+ // The following are non-standard extensions
+ selProto.getAllRanges = function() {
+ return this._ranges.slice(0);
+ };
+
+ selProto.setSingleRange = function(range) {
+ this.setRanges( [range] );
+ };
+
+ selProto.containsNode = function(node, allowPartial) {
+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
+ if (this._ranges[i].containsNode(node, allowPartial)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ function inspect(sel) {
+ var rangeInspects = [];
+ var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+ var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+ var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+ if (typeof sel.rangeCount != "undefined") {
+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+ rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+ }
+ }
+ return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+ ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+
+ }
+
+ selProto.getName = function() {
+ return "WrappedSelection";
+ };
+
+ selProto.inspect = function() {
+ return inspect(this);
+ };
+
+ selProto.detach = function() {
+ this.win[windowPropertyName] = null;
+ this.win = this.anchorNode = this.focusNode = null;
+ };
+
+ WrappedSelection.inspect = inspect;
+
+ api.Selection = WrappedSelection;
+
+ api.addCreateMissingNativeApiListener(function(win) {
+ if (typeof win.getSelection == "undefined") {
+ win.getSelection = function() {
+ return api.getSelection(this);
+ };
+ }
+ win = null;
+ });
+});
diff --git a/content/overlay.xul b/content/overlay.xul
index 7b685ca..0ea8d79 100644
--- a/content/overlay.xul
+++ b/content/overlay.xul
@@ -47,6 +47,7 @@
<script type="application/x-javascript" src="chrome://wot/content/api.js"/>
<script type="application/x-javascript" src="chrome://wot/content/firstrun.js"/>
<script type="application/x-javascript" src="chrome://wot/content/partner.js"/>
+ <script type="application/x-javascript" src="chrome://wot/content/wg.js"/>
<script type="application/x-javascript" src="chrome://wot/content/core.js"/>
<stringbundleset id="stringbundleset">
diff --git a/content/ratingwindow.js b/content/ratingwindow.js
index e770540..68b73d0 100644
--- a/content/ratingwindow.js
+++ b/content/ratingwindow.js
@@ -44,7 +44,7 @@ var wot_rw = {
return rw ? rw.wot : null;
},
- unseenmessage: function () {
+ message_seen: function (message_id) {
try {
if (wot_api_query.message_id.length > 0 &&
wot_api_query.message_id !=
@@ -52,7 +52,7 @@ var wot_rw = {
wot_prefs.setChar("last_message", wot_api_query.message_id);
}
} catch (e) {
- wot_tools.wdump("wot_rw.unseenmessage: failed with " + e);
+ wot_tools.wdump("wot_rw.message_seen: failed with " + e);
}
},
@@ -68,8 +68,6 @@ var wot_rw = {
{
this.is_visible = false;
try {
- wot_rw.unseenmessage();
-
var rw_wot = wot_rw.get_rw_wot();
rw_wot.ratingwindow.finishstate(true); // finish state with unload = true to indicate the unintentional closing of RW
@@ -131,12 +129,17 @@ var wot_rw = {
get_cached: function () {
var target = wot_core.hostname,
- data = {};
+ data = {},
+ wg = {};
- if (target && wot_cache.isok(target)) {
+ if (target && wot_cache.isok(target) && !wot_url.isprivate(target)) {
var normalized_target = wot_cache.get(target, "normalized") || target;
+ // Get WG data from the cache and merge it into
+ wg = wot_cache.get_param(target, "wg") || {};
+ wg.wg = wot_wg.is_enabled(); // override cached state by the actual
+
// prepare data for the RW
data = {
target: target,
@@ -146,7 +149,8 @@ var wot_rw = {
value: {
normalized: wot_shared.decodehostname(normalized_target)
},
- comment: wot_cache.get_comment(target)
+ comment: wot_cache.get_comment(target),
+ wg: wg
}
};
@@ -172,7 +176,7 @@ var wot_rw = {
} else {
data = {
- target: target,
+ target: null,
updated: null,
cached: {
status: WOT_QUERY_ERROR,
@@ -206,11 +210,9 @@ var wot_rw = {
rw_wot = this.get_rw_wot(),
target = wot_core.hostname;
-// wot_tools.wdump("\tTarget: " + target);
-
if (!rw || !rw_doc || !rw_wot) return;
- var data = wot_rw.get_cached();
+ var data = wot_rw.get_cached(); // no rep data for nonexistant targets
wot_rw.push_preferences(rw, wot_rw.get_preferences()); // update preferences every time before showing RW
@@ -233,8 +235,6 @@ var wot_rw = {
}
}
-// wot_tools.wdump("\tdata: " + JSON.stringify(data));
-
rw_wot.ratingwindow.update(target, JSON.stringify(data));
},
@@ -242,16 +242,31 @@ var wot_rw = {
var target = wot_core.hostname,
cached = wot_rw.get_cached(),
- rw = wot_rw.get_rw_window(),
rw_wot = wot_rw.get_rw_wot(),
local_comment = wot_keeper.get_comment(target); // get locally stored comment if exists
+ this.update_ratingwindow_tags();
+
rw_wot.ratingwindow.update_comment(cached.cached, local_comment, wot_cache.get_captcha());
},
+ update_ratingwindow_tags: function () {
+ var rw = wot_rw.get_rw_window(),
+ _core = rw.wot_bg.wot.core;
+
+ // Now fetch tags data and fill the variables in rating window space
+ if (_core) {
+ _core.tags.mytags_updated = wot_wg.get_mytags_updated();
+ _core.tags.popular_tags_updated = wot_wg.get_popular_tags_updated();
+ _core.tags.mytags = wot_wg.get_mytags();
+ _core.tags.popular_tags = wot_wg.get_popular_tags();
+ }
+ },
+
get_preferences: function () {
var prefs = {};
- try {
+
+ try {
prefs = {
accessible: wot_prefs.accessible,
show_fulllist: wot_prefs.show_fulllist,
@@ -259,15 +274,14 @@ var wot_rw = {
activity_score: wot_prefs.activity_score,
wt_rw_ok: wot_prefs.wt_rw_ok,
wt_rw_shown: wot_prefs.wt_rw_shown,
- wt_rw_shown_dt: wot_prefs.wt_rw_shown_dt
+ wt_rw_shown_dt: wot_prefs.wt_rw_shown_dt,
+ last_message: wot_prefs.last_message
};
} catch (e) {
wot_tools.wdump("ERROR: wot_rw.get_preferences() raised an exception: " + e);
}
-// wot_tools.wdump("prefs: " + JSON.stringify(prefs));
-
return prefs;
},
@@ -325,7 +339,7 @@ var wot_rw = {
var details = event.detail;
if (!details) return false;
-// wot_tools.wdump("on_ratingwindow_event() " + JSON.stringify(details));
+// wot_tools.log("on_ratingwindow_event()", details);
var message_id = details.message_id,
data = details.data;
@@ -345,8 +359,8 @@ var wot_rw = {
wot_rw.update_ratingwindow_comment();
break;
- case "unseenmessage":
- wot_rw.unseenmessage();
+ case "message_seen":
+ wot_rw.message_seen(data.message_id);
break;
case "navigate":
@@ -388,8 +402,13 @@ var wot_rw = {
wot_keeper.save_comment(data.target, data.user_comment, data.user_comment_id, data.votes, data.keeper_status);
break;
+ case "api_get_tags":
+// data.core_keyword , data.method
+ wot_api_tags.get_tags(data.core_keyword, data.method);
+ break;
+
case "log":
- wot_tools.wdump("LOG: " + JSON.stringify(data));
+ wot_tools.log("LOG: ", data);
break;
}
@@ -427,7 +446,6 @@ var wot_rw = {
var prefs = this.get_preferences();
this.push_preferences(rw, prefs);
-// wot_tools.wdump(JSON.stringify(prefs));
// setup categories data in the RW
rw_wot.categories = wot_categories.categories;
@@ -439,6 +457,8 @@ var wot_rw = {
// init other values
rw_wot.firstrunupdate = WOT_FIRSTRUN_CURRENT;
+ this.update_ratingwindow_tags();
+
rw_wot.ratingwindow.onload(); // this runs only once in FF
this.is_inited = true;
diff --git a/content/rw/proxies.js b/content/rw/proxies.js
index 30320f1..7ebdf08 100644
--- a/content/rw/proxies.js
+++ b/content/rw/proxies.js
@@ -1,6 +1,6 @@
/*
proxies.js
- Copyright © 2009 - 2013 WOT Services Oy <info at mywot.com>
+ Copyright © 2009 - 2014 WOT Services Oy <info at mywot.com>
This file is part of WOT.
@@ -72,7 +72,14 @@ $.extend(wot_bg.wot, wot, {
},
unseenmessage: function () {
- wot_bg.wot.core.moz_send("unseenmessage", null);
+
+ var _this = wot_bg.wot.core;
+
+ return (_this.usermessage &&
+ _this.usermessage.text &&
+ _this.usermessage.id &&
+ _this.usermessage.id != wot_bg.wot.prefs.get("last_message") &&
+ _this.usermessage.id != "downtime");
},
open_mywot: function(page, context) {
@@ -95,14 +102,28 @@ $.extend(wot_bg.wot, wot, {
moz_send: function (message_id, data) {
// Sends event with data to background code (outside of RatingWindow)
var obj = document.getElementById(wot_bg.wot.core._moz_element_id);
- var e = new CustomEvent(wot_bg.wot.core._moz_event_id, {
- "detail": {
- "message_id": message_id,
- "data": data
- }
- });
- obj.dispatchEvent(e);
- }
+ if (obj) {
+ var e = new CustomEvent(wot_bg.wot.core._moz_event_id, {
+ "detail": {
+ "message_id": message_id,
+ "data": data
+ }
+ });
+ obj.dispatchEvent(e);
+ }
+ },
+
+ tags: {
+
+ // these variables are updated from content/ratingwindow.js: update_ratingwindow_tags()
+ mytags: [ ],
+ mytags_updated: null, // time when the list was updated last time
+ MYTAGS_UPD_INTERVAL: 30 * 60 * 1000,
+
+ popular_tags: [ ],
+ popular_tags_updated: null,
+ POPULARTAGS_UPD_INTERVAL: 30 * 60 * 1000,
+ }
},
keeper: {
@@ -150,7 +171,21 @@ $.extend(wot_bg.wot, wot, {
wot_bg.wot.core.moz_send("remove_comment", { target: target });
}
- }
+ },
+
+ tags: {
+ my: {
+ get_tags: function () {
+ wot_bg.wot.core.moz_send("api_get_tags", { core_keyword: "mytags", method: "getmytags" });
+ }
+ },
+
+ popular: {
+ get_tags: function () {
+ wot_bg.wot.core.moz_send("api_get_tags", { core_keyword: "popular_tags", method: "getmastertags" });
+ }
+ }
+ }
},
diff --git a/content/rw/ratingwindow.html b/content/rw/ratingwindow.html
index 612937b..1eda209 100644
--- a/content/rw/ratingwindow.html
+++ b/content/rw/ratingwindow.html
@@ -2,7 +2,7 @@
<!--
ratingwindow.html
- Copyright © 2009 - 2013 WOT Services Oy <info at mywot.com>
+ Copyright © 2009 - 2014 WOT Services Oy <info at mywot.com>
This file is part of WOT.
@@ -21,189 +21,203 @@
-->
<html>
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <script type="text/javascript" src="chrome://wot/content/libs/jquery.js"></script>
- <script type="text/javascript" src="chrome://wot/content/libs/jquery.menu-aim.js"></script>
- <script type="text/javascript" src="chrome://wot/content/rw/wot.js"></script>
- <script type="text/javascript" src="chrome://wot/content/rw/proxies.js"></script>
- <script type="text/javascript" src="chrome://wot/content/rw/keeper_constants.js"></script>
- <script type="text/javascript" src="chrome://wot/content/injections/ga_configure.js"></script>
- <script type="text/javascript" src="chrome://wot/content/rw/ratingwindow.js"></script>
-
- <style type="text/css">
- @import "chrome://wot/skin/ratingslider.css";
- @import "chrome://wot/skin/ratingwindow.css";
- @import "chrome://wot/skin/welcometips.css";
- </style>
-</head>
-<body>
-<div id="wot-ratingwindow">
-
- <div id="wot-elements">
- <!-- header -->
- <div id="wot-header">
- <div id="header-line-1">
- <div id="wot-header-logo"></div>
- <div id="wot-header-links">
- <div id="wot-header-link-profile" class="wot-header-link"></div>
- <div id="wot-header-link-forum" class="wot-header-link"></div>
- <div id="wot-header-link-guide" class="wot-header-link"></div>
- <div id="wot-header-link-settings" class="wot-header-link"></div>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <script type="text/javascript" src="chrome://wot/content/libs/jquery.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/jquery.menu-aim.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/jquery-ui.min.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/bootstrap-typeahead.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/rangy-core.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/caret-position.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/libs/bootstrap-tagautocomplete.js"></script>
+
+ <script type="text/javascript" src="chrome://wot/content/rw/wot.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/rw/proxies.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/rw/keeper_constants.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/injections/ga_configure.js"></script>
+ <script type="text/javascript" src="chrome://wot/content/rw/ratingwindow.js"></script>
+
+ <style type="text/css">
+ @import "chrome://wot/skin/typeahead.css";
+ @import "chrome://wot/skin/ratingslider.css";
+ @import "chrome://wot/skin/ratingwindow.css";
+ @import "chrome://wot/skin/welcometips.css";
+ </style>
+ </head>
+ <body>
+ <div id="wot-ratingwindow">
+
+ <div id="wot-elements">
+ <!-- header -->
+ <div id="wot-header">
+ <div id="header-line-1">
+ <div id="wot-header-logo"></div>
+ <div id="wot-header-links">
+ <div id="message-indicator"></div>
+ <div id="wot-header-link-profile" class="wot-header-link"></div>
+ <div id="wot-header-link-forum" class="wot-header-link"></div>
+ <div id="wot-header-link-guide" class="wot-header-link"></div>
+ <div id="wot-header-link-settings" class="wot-header-link"></div>
+ </div>
+ <div id="wot-header-close"></div>
+ </div>
+ <div id="header-line-2">
+ <span id="wot-title-text"></span>
+ <span id="hostname-text"></span>
+ </div>
+ </div>
+
+ <div id="floating-message">
+ <div id="floating-message-text"></div>
</div>
- <div id="wot-header-close"></div>
- </div>
- <div id="header-line-2">
- <span id="wot-title-text"></span>
- <span id="hostname-text"></span>
- </div>
- </div>
- <div id="ratings-area">
- <!-- rating header -->
- <div id="wot-myratings-header">
- <div id="myrating-header"></div>
- <div id="wot-myrating-0-header"></div>
- <div id="wot-myrating-4-header"></div>
- </div>
- <!-- rating 0 / Trustworthiness -->
- <div id="wot-rating-0" class="wot-rating">
- <div id="wot-rating-0-data" class="wot-rating-data" component="0">
- <div id="wot-rating-0-testimony" class="wot-rating-testimony">
+ <div id="ratings-area">
+ <!-- rating header -->
+ <div id="wot-myratings-header">
+ <div id="myrating-header" class="title"></div>
+ <div id="wot-myrating-0-header"></div>
+ <div id="wot-myrating-4-header"></div>
+ </div>
+
+ <!-- Tiny thank you message -->
+ <div id="tiny-thankyou" class="thanks-text"></div>
+
+ <!-- rating 0 / Trustworthiness -->
+ <div id="wot-rating-0" class="wot-rating">
+ <div id="wot-rating-0-data" class="wot-rating-data" component="0">
+ <div id="wot-rating-0-testimony" class="wot-rating-testimony">
<div id="wot-rating-0-help" class="wot-rating-help">
<div id="wot-rating-0-helptext" class="wot-rating-helptext"></div>
<div id="wot-rating-0-helplink" class="wot-rating-helplink"></div>
- </div>
+ </div>
<div class="wot-rating-bounds">
<div id="wot-rating-0-boundleft" class="rating-bound-left"></div>
<div id="wot-rating-0-boundright" class="rating-bound-right"></div>
</div>
- <div id="wot-rating-0-stack" class="wot-rating-stack" component="0">
- <div id="wot-rating-0-slider" class="wot-rating-slider"></div>
- <div id="wot-rating-0-indicator" class="wot-rating-indicator"></div>
- </div>
- <div id="wot-rating-0-delete" class="rating-delete">
- <div id="wot-rating-0-deleteicon" class="rating-delete-icon"></div>
- </div>
+ <div id="wot-rating-0-stack" class="wot-rating-stack" component="0">
+ <div id="wot-rating-0-slider" class="wot-rating-slider"></div>
+ <div id="wot-rating-0-indicator" class="wot-rating-indicator"></div>
+ </div>
+ <div id="wot-rating-0-delete" class="rating-delete">
+ <div id="wot-rating-0-deleteicon" class="rating-delete-icon"></div>
+ </div>
+ </div>
</div>
</div>
- </div>
- <!-- rating 4 / Child Safety -->
- <div id="wot-rating-4" class="wot-rating">
- <div id="wot-rating-4-data" class="wot-rating-data" component="4">
- <div id="wot-rating-4-testimony" class="wot-rating-testimony">
+ <!-- rating 4 / Child Safety -->
+ <div id="wot-rating-4" class="wot-rating">
+ <div id="wot-rating-4-data" class="wot-rating-data" component="4">
+ <div id="wot-rating-4-testimony" class="wot-rating-testimony">
<div id="wot-rating-4-help" class="wot-rating-help">
<div id="wot-rating-4-helptext" class="wot-rating-helptext"></div>
<div id="wot-rating-4-helplink" class="wot-rating-helplink"></div>
- </div>
+ </div>
<div class="wot-rating-bounds">
<div id="wot-rating-4-boundleft" class="rating-bound-left"></div>
<div id="wot-rating-4-boundright" class="rating-bound-right"></div>
</div>
- <div id="wot-rating-4-stack" class="wot-rating-stack" component="4">
- <div id="wot-rating-4-slider" class="wot-rating-slider"></div>
- <div id="wot-rating-4-indicator" class="wot-rating-indicator"></div>
- </div>
- <div id="wot-rating-4-delete" class="rating-delete">
- <div id="wot-rating-4-deleteicon" class="rating-delete-icon"></div>
+ <div id="wot-rating-4-stack" class="wot-rating-stack" component="4">
+ <div id="wot-rating-4-slider" class="wot-rating-slider"></div>
+ <div id="wot-rating-4-indicator" class="wot-rating-indicator"></div>
+ </div>
+ <div id="wot-rating-4-delete" class="rating-delete">
+ <div id="wot-rating-4-deleteicon" class="rating-delete-icon"></div>
+ </div>
+ </div>
</div>
</div>
- </div>
- </div>
- <div class="user-comm-activity">
- <div id="wot-user-0-header" class="wot-user-header"></div>
- <div id="user-activityscore"></div>
- <div id="wot-user-0-notice" class="wot-user-notice"></div>
- </div>
+ <div class="user-comm-activity">
+ <div id="wot-user-0-header" class="wot-user-header"></div>
+ <div id="user-activityscore"></div>
+ <div id="wot-user-0-notice" class="wot-user-notice"></div>
+ </div>
- <div id="rated-votes">
- <div id="voted-categories">
- <div id="voted-categories-content"></div>
+ <div id="rated-votes">
+ <div id="voted-categories">
+ <div id="voted-categories-content"></div>
+ </div>
+ <div id="change-ratings" class="pseudo-link"></div>
+ </div>
</div>
- <div id="change-ratings"></div>
- </div>
- </div>
- <div id="main-area" class="view-mode">
- <div id="reputation-info">
- <div id="wot-rating-header-wot"></div>
- <div class="rep-info-sections">
- <div id="rep-block">
+ <div id="main-area" class="view-mode">
+ <div id="reputation-info">
+ <div id="wot-rating-header-wot" class="title"></div>
+ <div id="wot-scorecard-content">
+ <span id="wot-scorecard-visit" class="wot-scorecard-text link"></span>
+ </div>
+
+ <div class="rep-info-sections">
+ <div id="rep-block">
- <div id="rep-0" class="rep-tr-block">
- <div id="wot-rating-0-header" class="wot-rating-header"></div>
- <div class="rating-values">
- <div id="wot-rating-0-reputation" class="wot-rating-reputation"></div>
+ <div id="rep-0" class="rep-tr-block">
+ <div id="wot-rating-0-header" class="wot-rating-header"></div>
+ <div class="rating-values">
+ <div id="wot-rating-0-reputation" class="wot-rating-reputation"></div>
<div id="wot-rating-0-confidence" class="wot-rating-confidence"></div>
- <div class="rating-legend-wrapper">
- <div class="rating-legend"></div>
+ <div class="rating-legend-wrapper">
+ <div class="rating-legend"></div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div id="rep-4" class="rep-cs-block">
- <div id="wot-rating-4-header" class="wot-rating-header"></div>
- <div class="rating-values">
- <div id="wot-rating-4-reputation" class="wot-rating-reputation"></div>
+ <div id="rep-4" class="rep-cs-block">
+ <div id="wot-rating-4-header" class="wot-rating-header"></div>
+ <div class="rating-values">
+ <div id="wot-rating-4-reputation" class="wot-rating-reputation"></div>
<div id="wot-rating-4-confidence" class="wot-rating-confidence"></div>
- <div class="rating-legend-wrapper">
- <div class="rating-legend"></div>
+ <div class="rating-legend-wrapper">
+ <div class="rating-legend"></div>
+ </div>
+ </div>
</div>
</div>
- </div>
- <!-- TODO: simplify this -->
- <div id="wot-scorecard">
- <div id="wot-scorecard-content">
- <span id="wot-scorecard-visit" class="wot-scorecard-text"></span>
+ <div class="categories-area" style="display: block;">
+ <ul id="tr-categories-list"></ul>
</div>
</div>
- </div>
- <div class="categories-area" style="display: block;">
- <ul id="tr-categories-list"></ul>
</div>
- </div>
- </div>
-
- <!-- Welcome Tip area -->
- <div id="wot-welcometip" class="rtip-sticker">
- <div class="wt-rw-header">
- <span class="wt-rw-header-text">Share your experiences!</span>
- <div class="wt-rw-close"></div>
- </div>
- <div class="wt-rw-body">
- <p>WOT shows website reputations based on experiences from millions of users.</p>
- <p>Leave your own rating by clicking the colored bars to indicate what you think of the site. Your rating helps other users surf safer.</p>
- <p class="wot-c"><a id='wt-learnmore-link'>Learn more</a> about WOT.</p>
- </div>
- </div>
+ <!-- Welcome Tip area -->
+ <div id="wot-welcometip">
+ <div class="wt-rw-header">
+ <span class="wt-rw-header-text">Share your experiences!</span>
+ <div class="wt-rw-close"></div>
+ </div>
+ <div class="wt-rw-body">
+ <p>WOT shows website reputations based on experiences from millions of users.</p>
+ <p>Leave your own rating by clicking the colored bars to indicate what you think of the site. Your rating helps other users surf safer.</p>
+ <p class="wot-c"><a id='wt-learnmore-link'>Learn more</a> about WOT.</p>
+ </div>
+ </div>
- <!-- Rate mode: categories selector area -->
- <div id="categories-selection-area">
- <div class="category-description"></div>
- <div class="category-title"></div>
- <div class="category-selector">
- <ul class="dropdown-menu"></ul>
- </div>
+ <!-- Rate mode: categories selector area -->
+ <div id="categories-selection-area">
+ <div class="category-description"></div>
+ <div class="category-title"></div>
+ <div class="category-selector">
+ <ul class="dropdown-menu"></ul>
+ </div>
</div>
<div id="commenting-area">
<div class="comment-title"></div>
+ <div id="comment-top-hint">Use hash symbol (#) to link the website to a WOT Group.<br/>Example: I like this site about #cycling for the excellent #photos.</div>
<div id="comment-side-hint"></div>
<div id="comment-register">
<div id="comment-register-text"></div><br/>
- <div><span id="comment-register-link"></span></div>
+ <div><span id="comment-register-link" class="link"></span></div>
</div>
<div id="comment-captcha">
<div id="comment-captcha-text"></div><br/>
- <div><span id="comment-captcha-link"></span></div>
+ <div><span id="comment-captcha-link" class="link"></span></div>
</div>
<div class="user-comment-wrapper">
- <textarea id="user-comment" placeholder=""></textarea>
+ <div id="user-comment" placeholder="" contenteditable="true"></div>
<div id="comment-bottom-hint"></div>
</div>
</div>
@@ -213,37 +227,60 @@
<div class="thanks-activityscore">
<span class="thanks-activityscore-text"></span>
<span class="thanks-activityscore-number"></span>
- </div>
+ </div>
<div class="thanks-ratemore"></div>
</div>
+
+ <div id="wg-about-area">
+ <p class="text-title wg-about-title"></p>
+ <p class="wg-about-content"></p>
+ <div id="wg-about-ok" class="pseudo-link"></div>
+ <div id="wg-about-learnmore" class="link"></div>
+ </div>
</div>
- <div id="bottom-area">
+ <div id="bottom-area">
- <div id="user-communication">
- <div class="user-comm-social">
- <!-- message -->
- <div id="wot-message">
- <div id="wot-message-text"></div>
+ <div id="user-communication">
+ <div class="user-comm-social">
+ <!-- message -->
+ <div id="wot-message">
+ <div id="wot-message-text"></div>
+ </div>
+ <div class="social-buttons"></div>
+ </div>
</div>
- <div class="social-buttons">
- </div>
- </div>
- </div>
+ <div id="wg-area">
+ <div class="wg-title-bar">
+ <div id="wg-title" class="title"></div>
+ </div>
+ <div id="wg-right-bar">
+ <div id="wg-change"></div>
+ <div id="wg-about"></div>
+ <div id="wg-expander" class="pseudo-link hidden"></div>
+ </div>
- <div id="rate-buttons">
- <div class="buttons-wrapper">
- <div class="left-side">
- <div id="btn-delete" title="">
- <div class="btn-delete_icon"></div>
- <div class="btn-delete_label"></div>
+ <ul id="wg-tags"></ul>
+ <div id="wg-addmore" class="hidden"></div>
+ <div id="wg-viewer" style="display: none;">
+ <div class="wg-viewer-title"></div>
+ <iframe id="wg-viewer-frame" src=""></iframe>
</div>
</div>
- <div class="right-side">
- <div id="btn-comment" class="rw-button" title=""></div>
- <div id="btn-cancel" class="rw-button" title=""></div>
- <div id="btn-submit" class="rw-button btn-submit disabled" title=""></div>
+
+ <div id="rate-buttons">
+ <div class="buttons-wrapper">
+ <div class="left-side">
+ <div id="btn-delete" title="">
+ <div class="btn-delete_icon"></div>
+ <div class="btn-delete_label"></div>
+ </div>
+ </div>
+ <div class="right-side">
+ <div id="btn-comment" class="rw-button" title=""></div>
+ <div id="btn-cancel" class="rw-button" title=""></div>
+ <div id="btn-submit" class="rw-button btn-submit disabled" title=""></div>
</div>
</div>
</div>
@@ -253,26 +290,25 @@
<div class="left-side"></div>
<div class="right-side">
<div id="btn-thanks-ok" class="rw-button" title=""></div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </div>
- <!-- Old trash here -->
- <div id="wot-user-0-text" class="wot-user-text" style="display: none;"></div>
+ <!-- Old trash here -->
+ <div id="wot-user-0-text" class="wot-user-text" style="display: none;"></div>
- <div id="wot-user-0-bar" class="wot-user-bar" style="display: none;">
- <div id="wot-user-0-bar-image" class="wot-user-bar-image"></div>
- </div>
-
- <div id="wot-user-0-stack" class="wot-user-stack" style="display: none;"></div>
- <div id="wot-user-0-text-container" class="wot-user-text-container" style="display: none;"></div>
+ <div id="wot-user-0-bar" class="wot-user-bar" style="display: none;">
+ <div id="wot-user-0-bar-image" class="wot-user-bar-image"></div>
+ </div>
- </div>
+ <div id="wot-user-0-stack" class="wot-user-stack" style="display: none;"></div>
+ <div id="wot-user-0-text-container" class="wot-user-text-container" style="display: none;"></div>
-</div>
+ </div>
-<script type="text/javascript" src="chrome://wot/content/injections/ga_init.js"></script>
-</body>
+ </div>
+ <script type="text/javascript" src="chrome://wot/content/injections/ga_init.js"></script>
+ </body>
</html>
diff --git a/content/rw/ratingwindow.js b/content/rw/ratingwindow.js
index 4a8527c..0ca5f7b 100644
--- a/content/rw/ratingwindow.js
+++ b/content/rw/ratingwindow.js
@@ -1,6 +1,6 @@
/*
ratingwindow.js
- Copyright © 2009 - 2013 WOT Services Oy <info at mywot.com>
+ Copyright © 2009 - 2014 WOT Services Oy <info at mywot.com>
This file is part of WOT.
@@ -21,7 +21,7 @@
$.extend(wot, { ratingwindow: {
MAX_VOTED_VISIBLE: 4, // how many voted categories we can show in one line
sliderwidth: 154,
- slider_shift: -4, // ajustment
+ slider_shift: -4, // adjustment
opened_time: null,
was_in_ratemode: false,
timer_save_button: null,
@@ -29,11 +29,21 @@ $.extend(wot, { ratingwindow: {
local_comment: null,
is_registered: false, // whether user has an account on mywot.com
delete_action: false, // remembers whether user is deleting rating
- prefs: {}, // shortcut for background preferences
+ prefs: {}, // shortcut for background preferences
+ current: {},
- get_bg: function () {
+ is_wg_allowed: false,
+ tags: [], // WG tags for the current website
+ wg_viewer_timer: null,
+ wg_infotag_timer: null,
+ msg_timer: null,
+
+ get_bg: function (sub_objname) {
// just a shortcut
- return chrome.extension.getBackgroundPage();
+ var bg = chrome.extension.getBackgroundPage();
+ if (sub_objname && bg[sub_objname]) return bg[sub_objname];
+
+ return bg;
},
is_rated: function (state) {
@@ -71,6 +81,8 @@ $.extend(wot, { ratingwindow: {
_this.finishstate(false);
_this.state = { target: target, down: -1 };
_this.comments.set_comment(""); // reset comment field
+ $("#voted-categories-content").empty();
+ $("#rated-votes").removeClass("commented");
was_target_changed = true;
}
@@ -197,7 +209,9 @@ $.extend(wot, { ratingwindow: {
testimonies_changed = false,
comment_changed = false,
has_comment = false,
- user_comment = $("#user-comment").val().trim(),
+ user_comment = rw.comments.get_comment_value(),
+ mytags = rw.wg.get_tags(user_comment),
+ has_mytags = mytags && mytags.length,
user_comment_id = 0,
cached = {},
changed_votes = {}, // user votes diff as an object
@@ -213,12 +227,8 @@ $.extend(wot, { ratingwindow: {
bgwot.core.usermessage = bgwot.core.usermessage.previous;
}
- if (bgwot.core.unseenmessage()) {
- bgwot.prefs.set("last_message", bg.wot.core.usermessage.id);
- }
-
- if (rw.state.target) {
- target = rw.state.target;
+ if (rw.current.target) {
+ target = rw.current.target;
cached = rw.getcached();
is_rated = rw.is_rated(rw.state);
changed_votes = rw.cat_difference(is_rated);
@@ -238,11 +248,6 @@ $.extend(wot, { ratingwindow: {
}
}
-// bg.console.log("testimonies_changed:", testimonies_changed);
-// bg.console.log("comment_changed:", comment_changed);
-// bg.console.log("is_rated:", is_rated);
-// bg.console.log("has_comment:", has_comment);
-
/* if user's testimonies or categories were changed, store them in the cache and submit */
if (testimonies_changed) {
@@ -272,6 +277,7 @@ $.extend(wot, { ratingwindow: {
// count testimony event
if (is_rated) {
bgwot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_TESTIMONY, submission_mode);
+ bgwot.core.last_testimony = Date.now(); // remember when user rated last time
} else {
bgwot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_TESTIMONY_DEL, submission_mode);
}
@@ -281,10 +287,8 @@ $.extend(wot, { ratingwindow: {
}
if (unload) { // RW was closed by browser (not by clicking "Save")
-// bg.console.log("RW triggered finish state during Unload");
if (comment_changed) {
-// bg.console.log("The comment seems to be changed");
// when comment body is changed, we might want to store it locally
bgwot.keeper.save_comment(target, user_comment, user_comment_id, votes, wot.keeper.STATUSES.LOCAL);
bgwot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_COMMENTKEPT);
@@ -292,10 +296,9 @@ $.extend(wot, { ratingwindow: {
} else { // User clicked Save
// TODO: make it so, that if votes were changed and user have seen the comment, then submit the comment
- if (comment_changed && has_up_votes) {
+ if (comment_changed && (has_up_votes || has_mytags || !has_comment)) {
// Comment should be submitted, if (either comment OR categories votes were changed) AND at least one up vote is given
if (has_comment) {
-// bg.console.log("SUBMIT COMMENT");
// If user can't leave a comment for a reason, accept the comment locally, otherwise submit it silently
var keeper_status = (rw.comments.allow_commenting && rw.is_registered) ? wot.keeper.STATUSES.SUBMITTING : wot.keeper.STATUSES.LOCAL;
@@ -309,7 +312,6 @@ $.extend(wot, { ratingwindow: {
} else {
if (comment_changed) {
// remove the comment
-// bg.console.log("REMOVE COMMENT");
bgwot.keeper.remove_comment(target);
if (rw.is_registered) {
bgwot.api.comments.remove(target);
@@ -391,8 +393,6 @@ $.extend(wot, { ratingwindow: {
/* user interface */
- current: {},
-
updatecontents: function()
{
var bg = chrome.extension.getBackgroundPage(),
@@ -408,7 +408,7 @@ $.extend(wot, { ratingwindow: {
/* target */
if (_this.current.target && cached.status == wot.cachestatus.ok) {
- visible_hostname = bg.wot.url.decodehostname(normalized_target);
+ visible_hostname = normalized_target;
rw_title = wot.i18n("messages", "ready");
} else if (cached.status == wot.cachestatus.busy) {
rw_title = wot.i18n("messages", "loading");
@@ -421,7 +421,8 @@ $.extend(wot, { ratingwindow: {
$_hostname.text(visible_hostname);
$_wot_title_text.text(rw_title);
- $("#wot-ratingwindow").toggleClass("unregistered", !_this.is_registered);
+ $("#wot-ratingwindow")
+ .toggleClass("unregistered", !_this.is_registered);
/* reputations */
/* ratings */
@@ -452,18 +453,44 @@ $.extend(wot, { ratingwindow: {
/* message */
var msg = bg.wot.core.usermessage; // usual case: show a message from WOT server
- var $_wot_message = $("#wot-message");
+
+ // old style elements
+ var $_wot_message = $("#wot-message"),
+ $_wot_message_text = $("#wot-message-text");
+
+ // new style elements
+ var $msg_indicator = $("#message-indicator"),
+ $msg_box = $("#floating-message"),
+ $msg_text = $("#floating-message-text");
+
// if we have something to tell a user
if (msg.text) {
- var status = msg.type || "";
- $("#wot-message-text")
+ var status = msg.type || "",
+ is_seen = !bg.wot.core.unseenmessage();
+
+ $_wot_message_text
.attr("url", msg.url || "")
.attr("status", status)
.text(msg.text);
- $_wot_message.attr("status", status).attr("msg_id", msg.id).show();
+ $_wot_message.attr("status", status).attr("msg_id", msg.id).show();
+
+ $msg_text.text(msg.text);
+
+ $msg_box
+ .attr("url", msg.url || "")
+ .attr("status", status)
+ .attr("msg_id", msg.id);
+
+ $msg_indicator
+ .toggleClass("unseen", !is_seen)
+ .toggleClass("seen", is_seen)
+ .show();
+
} else {
$_wot_message.hide();
+ // hide the message icon on the top
+ $msg_indicator.hide();
}
/* content for user (messages / communications) */
@@ -508,6 +535,19 @@ $.extend(wot, { ratingwindow: {
$("#wot-user-0").css("display", "block");
}
+ /* this is a workaround for the bug in Firefox:
+ https://code.google.com/p/rangy/issues/detail?id=138
+ https://bugzilla.mozilla.org/show_bug.cgi?id=827585
+ when rangy gets null on window.getSelection() if the iframe is not visible to user.
+ So we simply re-init rangy on every update of the ratingwindow, thus making sure, rangy is inited properly.
+ * */
+ rangy.initialized = false; // FF specific
+ rangy.init(); // FF specific
+
+ // update WOT Groups UI
+ _this.wg.update_wg_visibility();
+ _this.wg.update_wg_tags();
+
},
insert_categories: function (cat_list, $_target) {
@@ -573,10 +613,15 @@ $.extend(wot, { ratingwindow: {
_rw.current = data || {};
_rw.is_registered = bg.wot.core.is_level("registered"); // update the state on every window update
- _rw.updatecontents();
+ // update visible content of the RW
+ _rw.updatecontents();
_rw.update_categories();
- if (_rw.is_registered) {
+ // Fetch mytags and popular tags whether they are expired or absent
+ _rw.wg.update_mytags(); // fetch user's tags whether they are not loaded yet or expired
+ _rw.wg.update_popular_tags();
+
+ if (_rw.is_registered) {
// ask server if there is my comment for the website
if (target_changed) { // no need to reask comment on every "iframe loaded" event
_rw.comments.get_comment(data.target);
@@ -602,7 +647,8 @@ $.extend(wot, { ratingwindow: {
var _rw = wot.ratingwindow,
_comments = wot.ratingwindow.comments,
- data = {},
+ comment_data = {},
+ wg = cached.wg || {},
bg = chrome.extension.getBackgroundPage(),
is_unsubmitted = false;
@@ -610,11 +656,20 @@ $.extend(wot, { ratingwindow: {
_rw.local_comment = local_comment; // keep locally stored comment
if (cached && cached.comment) {
- data = cached.comment;
+ comment_data = cached.comment;
_rw.comments.captcha_required = captcha_required || false;
}
- var error_code = data.error_code || 0;
+ // WOT Groups data
+ _rw.is_wg_allowed = wg.wg == true || false;
+ _rw.tags = [];
+
+ if (wg.tags && wg.tags.length > 0) {
+ _rw.tags = wg.tags;
+ }
+
+ // Errors
+ var error_code = comment_data.error_code || 0;
_comments.allow_commenting = ([
wot.comments.error_codes.AUTHENTICATION_FAILED,
@@ -628,27 +683,29 @@ $.extend(wot, { ratingwindow: {
if (local_comment && !wot.utils.isEmptyObject(local_comment)) {
// If server-side comment is newer, than drop locally stored one
- if (local_comment.timestamp && data.timestamp && data.timestamp >= local_comment.timestamp) {
+ if (local_comment.timestamp && comment_data.timestamp && comment_data.timestamp >= local_comment.timestamp) {
// Remove a comment from keeper
bg.wot.keeper.remove_comment(local_comment.target);
_rw.local_comment = null;
} else {
- data.comment = local_comment.comment;
- data.timestamp = local_comment.timestamp;
- data.wcid = data.wcid === undefined ? 0 : data.wcid;
+ comment_data.comment = local_comment.comment;
+ comment_data.timestamp = local_comment.timestamp;
+ comment_data.wcid = comment_data.wcid === undefined ? 0 : comment_data.wcid;
is_unsubmitted = true;
}
}
// check whether comment exists: "comment" should not be empty, and wcid should not be null (but it can be zero)
- if (data && data.comment && data.wcid !== undefined) {
- _comments.posted_comment = data;
- _comments.set_comment(data.comment);
+ if (comment_data && comment_data.comment && comment_data.wcid !== undefined) {
+ _comments.posted_comment = comment_data;
+ _comments.set_comment(comment_data.comment);
$("#rated-votes").addClass("commented");
// switch to commenting mode if we have unfinished comment
if (is_unsubmitted) {
_rw.modes.comment.activate();
+ } else {
+ _rw.modes.auto(true); // force to update the view
}
} else {
@@ -675,7 +732,8 @@ $.extend(wot, { ratingwindow: {
_rw.comments.show_normal_hint();
}
}
-
+ _rw.wg.update_wg_tags();
+ _rw.wg.update_wg_visibility();
_comments.update_button(_rw.modes.current_mode, _comments.allow_commenting && !_comments.is_banned);
},
@@ -687,8 +745,6 @@ $.extend(wot, { ratingwindow: {
count_window_opened: function () {
// increase amount of times RW was shown (store to preferences)
- wot.log("RW: count_window_opened");
-
var bg = chrome.extension.getBackgroundPage();
var counter = bg.wot.prefs.get(wot.engage_settings.invite_to_rw.pref_name);
counter = counter + 1;
@@ -744,7 +800,6 @@ $.extend(wot, { ratingwindow: {
{ selector: "#wot-rating-header-my", text: wot.i18n("ratingwindow", "myrating") },
{ selector: "#wot-scorecard-visit", text: wot.i18n("ratingwindow", "viewscorecard") },
{ selector: "#wot-scorecard-comment", text: wot.i18n("ratingwindow", "addcomment") },
-// { selector: "#wot-partner-text", text: wot.i18n("ratingwindow", "inpartnership") },
{ selector: ".wt-rw-header-text", html: wot.i18n("wt", "rw_text_hdr") },
{ selector: ".wt-rw-body", html: wot.i18n("wt", "rw_text") },
{ selector: ".btn-delete_label", text: wot.i18n("buttons", "delete") },
@@ -756,12 +811,20 @@ $.extend(wot, { ratingwindow: {
{ selector: "#change-ratings", text: wot.i18n("ratingwindow", "rerate_change") },
{ selector: ".comment-title", text: wot.i18n("ratingwindow", "comment") },
{ selector: "#user-comment", placeholder: wot.i18n("ratingwindow", "comment_placeholder") },
- { selector: "#comment-side-hint", html: wot.i18n("ratingwindow", "commenthints") },
{ selector: ".thanks-text", text: wot.i18n("ratingwindow", "thankyou") },
{ selector: "#comment-register-text", text: wot.i18n("ratingwindow", "comment_regtext") },
{ selector: "#comment-register-link", text: wot.i18n("ratingwindow", "comment_register") },
- { selector: "#comment-captcha-text", text: wot.i18n("ratingwindow", "comment_captchatext") },
- { selector: "#comment-captcha-link", text: wot.i18n("ratingwindow", "comment_captchalink") }
+ { selector: "#wg-title", html: wot.i18n("wg", "title") },
+ { selector: "#wg-addmore", text: wot.i18n("wg", "add_long") },
+ { selector: ".wg-viewer-title", text: wot.i18n("wg", "viewer_title_wikipedia") },
+ { selector: "#wg-expander", text: wot.i18n("wg", "expander") },
+ { selector: "#wg-about", text: wot.i18n("wg", "about") },
+ { selector: ".wg-about-title", text: wot.i18n("wg", "about_title") },
+ { selector: ".wg-about-content", text: wot.i18n("wg", "about_content") },
+ { selector: "#wg-about-ok", text: wot.i18n("wg", "about_ok") },
+ { selector: "#wg-about-learnmore", text: wot.i18n("wg", "about_more") },
+ { selector: "#comment-captcha-text", text: wot.i18n("ratingwindow", "comment_captchatext") },
+ { selector: "#comment-captcha-link", text: wot.i18n("ratingwindow", "comment_captchalink") }
].forEach(function(item) {
var $elem = $(item.selector);
@@ -886,26 +949,34 @@ $.extend(wot, { ratingwindow: {
has_comment = _rw.comments.is_commented(),
has_valid_comment = _rw.comments.has_valid_comment();
- // 1. Either TR or CS are rated, OR none of them are rated (e.g. "delete my ratings")
- for (i in wot.components) {
- var cmp = wot.components[i].name;
- if (_rw.state[cmp] && _rw.state[cmp].t !== null && _rw.state[cmp].t >= 0) {
- testimonies++;
- }
- }
+ if (_rw.modes.is_current("wgcomment")) { // quick comment & tag mode
- if (has_1upvote) {
- // if there is a comment, it must be valid, otherwise disallow the submit
- if ((testimonies > 0 && !has_comment) || has_valid_comment) { // if rated OR commented, then OK
- passed = true;
- } else if (testimonies == 0 && !has_comment) {
- passed = true;
- }
- } else {
- if (testimonies == 0 && has_comment == false) {
- passed = true; // no cats, no testimonies, no comment := "Delete everything" (if there are changes)
- }
- }
+ if ((has_comment && has_valid_comment) || !has_comment && _rw.comments.is_changed()) {
+ passed = true;
+ }
+
+ } else { // normal WOT rating mode
+ // 1. Either TR or CS are rated, OR none of them are rated (e.g. "delete my ratings")
+ for (var i in wot.components) {
+ var cmp = wot.components[i].name;
+ if (_rw.state[cmp] && _rw.state[cmp].t !== null && _rw.state[cmp].t >= 0) {
+ testimonies++;
+ }
+ }
+
+ if (has_1upvote) {
+ // if there is a comment, it must be valid, otherwise disallow the submit
+ if ((testimonies > 0 && !has_comment) || has_valid_comment) { // if rated OR commented, then OK
+ passed = true;
+ } else if (testimonies == 0 && !has_comment) {
+ passed = true;
+ }
+ } else {
+ if (testimonies == 0 && has_comment == false) {
+ passed = true; // no cats, no testimonies, no comment := "Delete everything" (if there are changes)
+ }
+ }
+ }
return passed;
},
@@ -928,17 +999,22 @@ $.extend(wot, { ratingwindow: {
$_submit.toggleClass("warning", !!warn);
// If user wants to delete ratings, change the text of the button and hide "Delete ratings" button
- if (enable && !_rw.is_rated(_rw.state) && !_rw.comments.has_valid_comment()) {
- $_submit.text(wot.i18n("testimony", "delete"));
- $("#btn-delete").hide();
- delete_action = true; // remember the reverse of the label
- }
+ if (_rw.modes.is_current("wgcomment")) {
+ delete_action = (_rw.comments.is_changed() && !_rw.comments.is_commented());
+ } else {
+ if (enable && !_rw.is_rated(_rw.state) && !_rw.comments.has_valid_comment()) {
+ delete_action = true; // remember the reverse of the label
+ }
+ }
}
if (!delete_action) {
$_submit.text(wot.i18n("buttons", "save"));
- $("#btn-delete").show();
+ } else {
+ $_submit.text(wot.i18n("testimony", "delete"));
+ $("#btn-delete").hide();
}
+
_rw.delete_action = delete_action;
},
@@ -971,11 +1047,12 @@ $.extend(wot, { ratingwindow: {
}
});
- $_wot_header_logo.bind("dblclick", function(event) {
- if (event.shiftKey) {
- wot.ratingwindow.navigate(chrome.extension.getURL("/settings.html"), wurls.contexts.rwlogo);
- }
- });
+ // No such feature in Firefox
+// $_wot_header_logo.bind("dblclick", function(event) {
+// if (event.shiftKey) {
+// wot.ratingwindow.navigate(chrome.extension.getURL("/settings.html"), wurls.contexts.rwlogo);
+// }
+// });
$("#wot-header-link-settings").bind("click", function() {
wot.ratingwindow.navigate(wurls.settings, wurls.contexts.rwsettings);
@@ -1015,7 +1092,7 @@ $.extend(wot, { ratingwindow: {
}
});
- $("#wot-message").bind("click", function() {
+ $("#wot-message, #floating-message").bind("click", function() {
var url = $("#wot-message-text").attr("url");
if (url) {
var label = wot.i18n("locale") + "__" + $(this).attr("msg_id");
@@ -1024,23 +1101,67 @@ $.extend(wot, { ratingwindow: {
}
});
+ $("#message-indicator")
+ .bind({
+ "mouseenter": function() {
+ $("#floating-message").fadeIn(200);
+ $(this).addClass("seen").removeClass("unseen");
+ // remember the message as read
+ var bgwot = wot.ratingwindow.get_bg("wot");
+ bgwot.core.moz_send("message_seen", { message_id: bgwot.core.usermessage.id });
+ if (_rw.msg_timer) clearTimeout(_rw.msg_timer);
+ },
+
+ "mouseleave": function() {
+ _rw.msg_timer = setTimeout(function (){
+ $("#floating-message").fadeOut(200);
+ },500);
+ }
+ });
+
+ $("#floating-message").bind({
+ "mouseenter": function() {
+ if (_rw.msg_timer) clearTimeout(_rw.msg_timer);
+ },
+
+ "mouseleave": function() {
+ if (_rw.msg_timer) clearTimeout(_rw.msg_timer);
+ _rw.msg_timer = setTimeout(function (){
+ $("#floating-message").fadeOut(200);
+ },500);
+ }
+ });
+
$(".rating-delete-icon, .rating-deletelabel").bind("click", _rw.rate_control.on_remove);
- $("#user-comment").bind("change keyup", function() {
- window.setTimeout(function(){
- wot.ratingwindow.comments.update_hint();
+ $("#user-comment")
+ .bind("change keyup", function(event) {
- // set the timeout to update save button when user stops typing the comment
- if (wot.ratingwindow.timer_save_button) {
- window.clearTimeout(wot.ratingwindow.timer_save_button);
- }
- wot.ratingwindow.timer_save_button = window.setTimeout(wot.ratingwindow.update_submit_button, 200);
+ wot.ratingwindow.comments.update_caret(event, this);
- }, 20); // to react on any keyboard event after the text was changed
- });
+ window.setTimeout(function(){
+ wot.ratingwindow.comments.update_hint();
+ wot.ratingwindow.wg.update_wg_tags();
+
+ // set the timeout to update save button when user stops typing the comment
+ if (wot.ratingwindow.timer_save_button) {
+ window.clearTimeout(wot.ratingwindow.timer_save_button);
+ }
+ wot.ratingwindow.timer_save_button = window.setTimeout(function(){
+ wot.ratingwindow.update_submit_button();
+ }, 200);
+
+ }, 20); // to react on any keyboard event after the text was changed
+ })
+ .tagautocomplete({
+ source: wot.ratingwindow.wg.suggest_tags,
+ character: "#",
+ items: 4,
+ show: wot.ratingwindow.wg.show_tagautocomplete
+ });
// Rate mode event handlers
- $("#btn-comment").bind("click", _rw.on_comment_button);
+ $("#btn-comment").unbind("click").bind("click", _rw.on_comment_button);
$("#btn-submit").bind("click", _rw.on_submit);
$("#btn-thanks-ok").bind("click", _rw.on_thanks_ok);
$("#btn-cancel").bind("click", _rw.on_cancel);
@@ -1065,8 +1186,8 @@ $.extend(wot, { ratingwindow: {
_rw.rate_control.init(); // init handlers of rating controls
bg.wot.core.update(true); // this starts main data initialization (e.g. before it, there is no "cached" data)
- var wt = bg.wot.wt,
- locale = wot.i18n("locale");
+ var wt = bg.wot.wt,
+ locale = bg.wot.i18n("locale");
// Welcome Tip button "close"
$(".wt-rw-close").bind("click", function (e){
@@ -1106,7 +1227,10 @@ $.extend(wot, { ratingwindow: {
wt.save_setting("rw_shown_dt");
}
- // increment "RatingWindow shown" counter
+ // Web Guide initialization
+ _rw.wg.init_handlers();
+
+ // increment "RatingWindow shown" counter
_rw.count_window_opened();
bg.wot.core.badge.text = "";
bg.wot.core.badge.type = null;
@@ -1116,21 +1240,43 @@ $.extend(wot, { ratingwindow: {
// if (bg.wot.core.badge_status && bg.wot.core.badge_status.type == wot.badge_types.notice.type) {
// bg.wot.core.set_badge(null, false); // hide badge
// }
+
+ _rw.modes.unrated.activate(true);
+
+ _rw.wg.update_mytags(); // fetch user's tags whether they are not loaded yet or expired
+ _rw.wg.update_popular_tags();
},
+ show_tiny_thankyou: function () {
+ $("#tiny-thankyou").fadeIn(500, function (){
+ window.setTimeout(function (){
+ $("#tiny-thankyou").fadeOut(1000);
+ }, 2000);
+ });
+ },
+
on_comment_button: function (e) {
var _rw = wot.ratingwindow;
+// _rw.get_bg().console.log("on_comment_button");
if ($(this).hasClass("disable")) return; // do nothing of the button is disabled
switch (_rw.modes.current_mode) {
case "rate":
- if (!_rw.comments.allow_commenting) return;
+ if (!_rw.comments.allow_commenting) return;
_rw.update_uservoted();
- _rw.modes.comment.activate();
+ _rw.modes.comment.activate();
+
+ // do some stats collection
+ if (_rw.comments.is_commented()) {
+ wot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_EDITCOMMENT);
+ } else {
+ wot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_ADDCOMMENT);
+ }
break;
case "comment":
_rw.modes.rate.activate();
+ wot.ga.fire_event(wot.ga.categories.RW, wot.ga.actions.RW_PICKACAT);
break;
}
},
@@ -1187,16 +1333,27 @@ $.extend(wot, { ratingwindow: {
on_submit: function (e) {
// console.log("on_submit()");
- if ($(e.currentTarget).hasClass("disabled")) return; // do nothing is "Save" is not allowed
+ if ($(e.currentTarget).hasClass("disabled")) return; // do nothing is "Save" is not allowed
- var _rw = wot.ratingwindow;
- wot.ratingwindow.finishstate(false);
- if (_rw.delete_action) {
+ var _rw = wot.ratingwindow,
+ bg = _rw.get_bg(),
+ last_rated = 0 + bg.wot.core.last_testimony; // remember the value before saving the testimony
+
+ wot.ratingwindow.finishstate(false);
+ if (_rw.delete_action) {
_rw.get_bg().wot.core.update(true);
- _rw.modes.auto(); // switch RW mode according to current state
- } else {
- _rw.modes.thanks.activate();
- }
+ _rw.modes.auto(); // switch RW mode according to current state
+ } else {
+ if ((last_rated == 0 || (Date.now() - last_rated) > wot.TINY_THANKYOU_DURING) &&
+ !_rw.modes.is_current("wgcomment")) {
+ // show full Thank You screen when last rating from the user was long time ago
+ _rw.modes.thanks.activate();
+ } else {
+ // otherwise show tiny thank you
+ _rw.modes.auto();
+ _rw.show_tiny_thankyou();
+ }
+ }
},
on_thanks_ok: function () {
@@ -1282,8 +1439,7 @@ $.extend(wot, { ratingwindow: {
$_ratingarea = $("#ratings-area");
if (mode == "unrated") {
- var cached = _rw.getcached();
- if (_rw.state.target) {
+ if (_rw.current.target ) {
$_ratingarea.attr("disabled", null);
} else {
$_ratingarea.attr("disabled", "disabled");
@@ -1352,10 +1508,19 @@ $.extend(wot, { ratingwindow: {
var helptext = wot.get_level_label(item.name, rep, true);
if (helptext.length) {
- elems.helptext.text(helptext).show();
- elems.helptext.attr("r", rep);
+ var show_helptext = true;
+ if (rep == "r0" && _rw.prefs.get("activity_score") >= 3000) {
+ show_helptext = false;
+ }
+
+ if (show_helptext) {
+ elems.helptext.text(helptext).show();
+ elems.helptext.attr("r", rep);
+ } else {
+ elems.helptext.text("");
+ }
} else {
- elems.helptext.hide();
+ elems.helptext.hide();
}
});
@@ -1369,44 +1534,114 @@ $.extend(wot, { ratingwindow: {
current_mode: "",
unrated: {
- visible: ["#reputation-info", "#user-communication", ".user-comm-social"],
+ visible: ["#ratings-area", "#reputation-info", "#user-communication", ".user-comm-social", "#main-area"],
invisible: ["#rate-buttons", "#categories-selection-area", "#rated-votes",
- "#commenting-area", "#thanks-area", "#ok-button"],
+ "#commenting-area", "#thanks-area", "#ok-button", "#wg-about-area"],
addclass: "view-mode unrated",
- removeclass: "rated commenting thanks rate",
+ removeclass: "rated commenting thanks rate wgcommenting wgexpanded wgabout",
+
+ show_effect: {
+ name: "fade",
+ direction: "in"
+ },
- activate: function () {
- if (!wot.ratingwindow.modes._activate("unrated")) return false;
+ show_duration: 100,
+ hide_duration: 0,
+
+ before_show: function (prev_mode) {
+ $("#main-area")[0].style.height = null;
+ },
+
+ before_hide: function (new_mode) {
+ var $mainarea = $("#main-area");
+
+ if (new_mode == "wgcomment") {
+ $mainarea[0].style.height = wot.ratingwindow.modes.unrated.get_mainarea_height($mainarea);
+ }
+ },
+
+ get_mainarea_height: function ($elem) {
+
+ var e = $elem[0],
+ rects = e.getClientRects(),
+ rect = rects && rects.length ? rects[0] : {},
+ h = rect.height || 0;
+
+ return h + $("#ratings-area").height() - (wot.platform != "firefox"? 1 : 0) + "px";
+ },
+
+ activate: function (force) {
+ if (!wot.ratingwindow.modes._activate("unrated", force) && !force) return false;
+
+ var $rated_votes = $("#rated-votes"),
+ show_comment_icon = $rated_votes.hasClass("commented");
+
+ $rated_votes.toggle(show_comment_icon);
+ $(".user-comm-activity").toggle(!show_comment_icon);
+
+ wot.ratingwindow.wg.update_wg_visibility();
return true;
}
},
rated: {
- visible: ["#reputation-info", "#user-communication", "#rated-votes", ".user-comm-social"],
+ visible: ["#ratings-area", "#reputation-info", "#user-communication", "#rated-votes", ".user-comm-social", "#main-area"],
invisible: ["#rate-buttons", "#categories-selection-area",
- "#commenting-area", "#thanks-area", "#ok-button"],
+ "#commenting-area", "#thanks-area", "#ok-button", "#wg-about-area"],
addclass: "view-mode rated",
- removeclass: "unrated commenting thanks rate",
+ removeclass: "unrated commenting thanks rate wgcommenting wgexpanded wgabout",
+
+ show_effect: {
+ name: "fade",
+ direction: "in"
+ },
+
+ show_duration: 100,
+ hide_duration: 0,
+
+ before_show: function (prev_mode) {
+ $("#main-area")[0].style.height = null;
+ },
+
+ before_hide: function (new_mode) {
+ var $mainarea = $("#main-area");
+
+ if (new_mode == "wgcomment") {
+ $mainarea[0].style.height = wot.ratingwindow.modes.unrated.get_mainarea_height($mainarea);
+ }
+ },
+
+ activate: function (force) {
+ if (!wot.ratingwindow.modes._activate("rated", force) && !force) return false;
+
+ $(".user-comm-activity").hide(); // in rated mode never show activity score line
- activate: function () {
- if (!wot.ratingwindow.modes._activate("rated")) return false;
wot.ratingwindow.update_uservoted();
+ wot.ratingwindow.wg.update_wg_visibility();
return true;
}
},
rate: {
- visible: ["#rate-buttons", "#categories-selection-area"],
+ visible: ["#ratings-area", "#rate-buttons", "#categories-selection-area", "#main-area"],
invisible: ["#reputation-info", "#user-communication", "#rated-votes",
- "#commenting-area", "#thanks-area", "#ok-button"],
+ "#commenting-area", "#thanks-area", "#ok-button", "#wg-area", "#wg-about-area"],
addclass: "rate",
- removeclass: "view-mode rated unrated commenting thanks",
+ removeclass: "view-mode rated unrated commenting thanks wgcommenting wgexpanded wgabout",
+
+ show_effect: {
+ name: "fade",
+ direction: "in"
+ },
+
+ show_duration: 100,
+ hide_duration: 0,
- activate: function () {
+ activate: function (force) {
var _rw = wot.ratingwindow,
prev_mode = _rw.modes.current_mode;
- if (!_rw.modes._activate("rate")) return false;
+ if (!_rw.modes._activate("rate", force) && !force) return false;
// "Comment" mode can be the first active mode in session, so we have to init things still.
if (prev_mode != "comment" || !_rw.cat_selector.inited) {
@@ -1418,7 +1653,7 @@ $.extend(wot, { ratingwindow: {
_rw.update_catsel_state(); // update the category selector with current state
}
- _rw.cat_selector.calc_illogicality();
+ _rw.cat_selector.calc_illogicality();
_rw.cat_selector.warn_illogicality(_rw.cat_selector.is_illogical);
_rw.update_submit_button(null, _rw.cat_selector.is_illogical);
@@ -1430,17 +1665,25 @@ $.extend(wot, { ratingwindow: {
}
},
- comment: { // Not implemented yet
- visible: ["#rate-buttons", "#commenting-area", "#rated-votes"],
+ comment: { // Commenting during rating process
+ visible: ["#ratings-area", "#rate-buttons", "#commenting-area", "#rated-votes", "#main-area"],
invisible: ["#reputation-info", "#user-communication", "#categories-selection-area",
- "#thanks-area", "#ok-button"],
+ "#thanks-area", "#ok-button", "#wg-area", "#wg-about-area"],
addclass: "commenting",
- removeclass: "view-mode rated unrated rate thanks",
+ removeclass: "view-mode rated unrated rate thanks wgcommenting wgexpanded wgabout",
- activate: function () {
+ show_effect: {
+ name: "fade",
+ direction: "in"
+ },
+
+ show_duration: 100,
+ hide_duration: 0,
+
+ activate: function (force) {
var _rw = wot.ratingwindow,
prev_mode = _rw.modes.current_mode;
- if (!wot.ratingwindow.modes._activate("comment")) return false;
+ if (!wot.ratingwindow.modes._activate("comment", force) && !force) return false;
// TODO: this piece of code is a duplication. Should be refactored.
if (prev_mode == "" || !_rw.cat_selector.inited) {
@@ -1449,8 +1692,12 @@ $.extend(wot, { ratingwindow: {
_rw.cat_selector.init();
}
_rw.cat_selector.init_voted();
+ _rw.update_catsel_state(); // update the category selector with current state
}
+ // update side hint to the relevant hint
+ $("#comment-side-hint").html(wot.i18n("ratingwindow", "commenthints"));
+
_rw.was_in_ratemode = true; // since in comment mode user is able to change rating, we should set the flag
_rw.comments.update_hint();
_rw.comments.update_button("comment", true);
@@ -1461,16 +1708,116 @@ $.extend(wot, { ratingwindow: {
}
},
+ wgcomment: { // Quick Comment mode for WebGuide feature
+ visible: ["#wg-area", "#commenting-area", "#main-area"], // "#rate-buttons" will be shown after animation
+ invisible: ["#ratings-area", "#reputation-info", "#user-communication", "#categories-selection-area",
+ "#thanks-area", "#ok-button", "#rated-votes", "#wg-about-area"],
+
+ addclass: "wgcommenting",
+ removeclass: "view-mode rated unrated rate thanks wgexpanded wgabout",
+
+ show_effect: {
+ name: "blind",
+ direction: "down"
+ },
+
+ hide_effect: {
+ name: null
+ },
+
+ show_duration: 200,
+ hide_duration: 0,
+
+ before_show: function () {
+ },
+
+ before_hide: function () {
+ $("#rate-buttons").hide();
+ },
+
+ after_show: function () {
+ $("#rate-buttons").show();
+ },
+
+ activate: function (force) {
+ var _rw = wot.ratingwindow,
+ prev_mode = _rw.modes.current_mode;
+ if (!wot.ratingwindow.modes._activate("wgcomment", force) && !force) return false;
+
+ // update side hint to the relevant hint
+ $("#comment-side-hint").html(wot.i18n("ratingwindow", "wgcommenthints"));
+
+ _rw.was_in_ratemode = false; // user is commenting passing rating process
+ _rw.comments.update_hint();
+ _rw.update_submit_button();
+ _rw.reveal_ratingwindow(true);
+ _rw.comments.focus();
+ return true;
+ }
+ },
+
+ wgexpanded: { // Full view of WOT Groups feature
+ visible: ["#wg-area", "#ratings-area" ],
+ invisible: [ "#reputation-info", "#user-communication", "#categories-selection-area",
+ "#thanks-area", "#ok-button", "#commenting-area", "#wg-about-area", "#ok-button"],
+
+ addclass: "wgexpanded",
+ removeclass: "rate thanks wgcommenting commenting wgabout",
+
+ show_duration: 300,
+ hide_duration: 0,
+
+ before_show: function () {
+ $("#main-area").hide({
+ effect: "blind",
+ direction: "up",
+ duration: 500,
+ easing: "easeOutQuart",
+ complete: function () {
+ $("#wg-tags").addClass("expanded");
+ }
+ });
+ },
+
+ before_hide: function () {
+ wot.ratingwindow.modes.wgexpanded.reset();
+ },
+
+ after_show: function () {
+ },
+
+ after_hide: function () {
+ wot.ratingwindow.wg.update_wg_tags();
+ },
+
+ reset: function () {
+ $("#wg-expander").text(wot.i18n("wg", "expander"));
+ $("#wg-tags").removeClass("expanded");
+ },
+
+ activate: function (force) {
+ var _rw = wot.ratingwindow,
+ prev_mode = _rw.modes.current_mode;
+ if (!wot.ratingwindow.modes._activate("wgexpanded", force) && !force) return false;
+
+ _rw.was_in_ratemode = false; // user is commenting passing rating process
+ _rw.reveal_ratingwindow(true);
+ $("#wg-expander").text(wot.i18n("wg", "expander_less"));
+
+ return true;
+ }
+ },
+
thanks: {
- visible: ["#thanks-area", "#rated-votes", "#ok-button"],
+ visible: ["#thanks-area", "#ratings-area", "#rated-votes", "#ok-button", "#main-area"],
invisible: ["#reputation-info", "#user-communication", "#categories-selection-area",
- "#commenting-area", "#rate-buttons"],
+ "#commenting-area", "#rate-buttons", "#wg-area", "#wg-about-area"],
addclass: "thanks view-mode",
- removeclass: "rated unrated rate commenting",
+ removeclass: "rated unrated rate commenting wgcommenting wgexpanded wgabout",
- activate: function () {
+ activate: function (force) {
var _rw = wot.ratingwindow;
- if (!_rw.modes._activate("thanks")) return false;
+ if (!_rw.modes._activate("thanks", force) && !force) return false;
_rw.update_uservoted();
@@ -1486,39 +1833,98 @@ $.extend(wot, { ratingwindow: {
}
},
- show_hide: function (mode_name) {
- var _modes = wot.ratingwindow.modes;
- var visible = _modes[mode_name] ? _modes[mode_name].visible : [];
- var invisible = _modes[mode_name] ? _modes[mode_name].invisible : [];
+ wg_about: {
+ // the explanation screen "What's this?" for WOT Groups
+ visible: ["#wg-area", "#wg-about-area", "#ratings-area", "#main-area"],
+ invisible: ["#reputation-info", "#user-communication", "#categories-selection-area",
+ "#commenting-area", "#rate-buttons" ],
+ addclass: "wgabout view-mode",
+ removeclass: "rated unrated rate commenting thanks wgcommenting wgexpanded",
+
+ before_hide: function (new_mode) {
+ var $mainarea = $("#main-area");
+
+ if (new_mode == "wgcomment") {
+ $mainarea[0].style.height = wot.ratingwindow.modes.unrated.get_mainarea_height($mainarea);
+ }
+ },
- $(invisible.join(", ")).hide();
- $("#wot-ratingwindow").addClass(_modes[mode_name].addclass).removeClass(_modes[mode_name].removeclass);
- $(visible.join(", ")).show();
+ activate: function (force) {
+ var _rw = wot.ratingwindow;
+ if (!_rw.modes._activate("wg_about", force) && !force) return false;
+
+ $("#main-area")[0].style.height = null;
+
+ return true;
+ }
+ },
+
+ show_hide: function (mode_name) {
+ var _modes = wot.ratingwindow.modes,
+ current_mode = _modes.current_mode,
+ mode = _modes[mode_name],
+ cmode = _modes[current_mode] || {};
+ var visible = mode ? mode.visible : [];
+ var invisible = mode ? mode.invisible : [];
+
+ if (cmode && typeof(cmode.before_hide) == "function") cmode.before_hide(mode_name);
+
+ var hide_effect = cmode.hide_effect ? cmode.hide_effect.name : "",
+ show_effect = mode.show_effect ? mode.show_effect.name : "",
+ hide_params = cmode.hide_effect ? cmode.hide_effect : {},
+ show_params = mode.show_effect ? mode.show_effect : {};
+
+ var hide_options = {
+ effect: hide_effect,
+ duration: cmode && cmode.hide_duration ? cmode.hide_duration : 0,
+ complete: function () {
+ if (current_mode && typeof(cmode.after_hide) == "function") cmode.after_hide(mode_name);
+
+ // then switch classes
+ $("#wot-ratingwindow").addClass(mode.addclass).removeClass(mode.removeclass);
+ }
+ };
+
+ hide_options = $.extend(hide_options, hide_params);
+ $(invisible.join(", ")).hide(hide_options);
+
+ // then show new mode
+ if (typeof(mode.before_show) == "function") mode.before_show(current_mode);
+
+ var show_options = {
+ effect: show_effect,
+ duration: mode && mode.show_duration ? mode.show_duration : 0,
+ complete: function () {
+ if (typeof(mode.after_show) == "function") mode.after_show(current_mode);
+ }
+ };
+ show_options = $.extend(show_options, show_params);
+ $(visible.join(", ")).show(show_options);
},
- _activate: function (mode_name) {
+ _activate: function (mode_name, force) {
/* Generic func to do common things for switching modes. Returns false if there is no need to switch the mode. */
// console.log("RW.modes.activate(" + mode_name + ")");
var _rw = wot.ratingwindow;
- if (_rw.modes.current_mode == mode_name) return false;
+ if (_rw.modes.current_mode == mode_name && !force) return false;
_rw.modes.show_hide(mode_name);
_rw.modes.current_mode = mode_name;
_rw.rate_control.update_ratings_visibility(mode_name);
- return true;
+ return true;
},
- auto: function () {
+ auto: function (enforce) {
var _rw = wot.ratingwindow;
if (_rw.local_comment && _rw.local_comment.comment) {
- _rw.modes.comment.activate();
+ _rw.modes.comment.activate(enforce);
} else {
// If no locally saved comment exists, switch modes between Rated / Unrated
if (_rw.is_rated()) {
- _rw.modes.rated.activate();
+ _rw.modes.rated.activate(enforce);
} else {
- _rw.modes.unrated.activate();
+ _rw.modes.unrated.activate(enforce);
}
}
},
@@ -2082,15 +2488,43 @@ $.extend(wot, { ratingwindow: {
is_banned: false,
captcha_required: false,
MIN_LIMIT: 30,
+ MIN_LIMIT_WG: 3,
+ MIN_TAGS: 1, // minimal amount of tags in the comment
+ MAX_TAGS: 10, // maximum amount of tags in the comment
MAX_LIMIT: 20000,
- is_changed: false,
posted_comment: {},
+ caret_left: null,
+ caret_top: null,
+ caret_bottom: null,
+ AUTOCOMPLETE_OFFSET_X: -20,
+
+ get_comment_value: function (need_html) {
+ var elem = $("#user-comment")[0],
+ s = need_html ? elem.innerHTML : elem.textContent; // different in Firefox
+
+ s = typeof(s) == "string" ? s.trim() : "";
+
+ return s;
+ },
+
is_commented: function() {
// comment can be there, but it can be invalid (outside of limits restrictions, etc)
- return ($("#user-comment").val().trim().length > 0);
+ return (wot.ratingwindow.comments.get_comment_value().length > 0);
},
+ is_changed: function () {
+ var rw = wot.ratingwindow,
+ _this = rw.comments,
+ cached = rw.getcached(),
+ prev_comment = "",
+ current_comment = _this.get_comment_value();
+
+ prev_comment = (cached.comment && cached.comment.comment) ? cached.comment.comment : "";
+
+ return current_comment != prev_comment;
+ },
+
get_comment: function (target) {
var bg = wot.ratingwindow.get_bg(),
bgwot = bg.wot;
@@ -2104,26 +2538,67 @@ $.extend(wot, { ratingwindow: {
// TODO: to be implemented when there will be a button "remove the comment" in UI
},
+ get_minlen: function (is_wg) {
+ var _this = wot.ratingwindow.comments;
+ return is_wg ? _this.MIN_LIMIT_WG : _this.MIN_LIMIT;
+ },
+
+ get_maxlen: function (is_wg) {
+ var _this = wot.ratingwindow.comments;
+ return _this.MAX_LIMIT;
+ },
+
update_hint: function () {
+ // shows / hides a error hint if comment parameters don't fit our requirements
var rw = wot.ratingwindow,
_this = rw.comments,
- $_comment = $("#user-comment"),
$_hint = $("#comment-bottom-hint"),
- len = $_comment.val().trim().length,
- fix_len = 0,
+ is_wg = wot.ratingwindow.modes.is_current("wgcomment") || rw.is_wg_allowed,
+ is_wg_mode = wot.ratingwindow.modes.is_current("wgcomment"),
+ len = _this.get_comment_value().length,
+ error_text = 0,
+ errors = [],
cls = "";
- if (len > 0 && len < _this.MIN_LIMIT) {
- fix_len = String(len - _this.MIN_LIMIT).replace("-", "– "); // readability is our everything
- cls = "error min"
- } else if (len > _this.MAX_LIMIT) {
- fix_len = len - _this.MAX_LIMIT;
- cls = "error max"
- } else {
- // we could show here something like "looks good!"
+ var _wg = rw.wg,
+ tags = rw.is_wg_allowed ? _wg.get_tags() : [], // count tags only if WG is enabled for the user
+ tags_num = tags.length,
+ valid_tagged = rw.is_wg_allowed && tags_num >= _this.MIN_TAGS;
+
+ var min_len = _this.get_minlen(valid_tagged),
+ max_len = _this.get_maxlen(valid_tagged);
+
+ errors.push({ text: error_text, cls: cls }); // initial "no errors" state
+
+ if (len > 0 && len < min_len) {
+ errors.push({
+ text: String(len - min_len).replace("-", "– "), // readability is our everything
+ cls: "error min"
+ });
+ } else if (len > max_len) {
+ errors.push({
+ text: len - max_len,
+ cls: "error max"
+ });
}
- $_hint.attr("class", cls).text(fix_len);
+ // in WG comment mode we check number of hashtags first
+ if (is_wg_mode && len > 0) {
+ if (tags_num < _this.MIN_TAGS) {
+ errors.push({
+ text: "– " + String(_this.MIN_TAGS - tags_num) + " #",
+ cls: "error min"
+ });
+ } else if (tags_num > _this.MAX_TAGS) {
+ errors.push({
+ text: "> " + String(tags_num - _this.MIN_TAGS) + " #",
+ cls: "error max"
+ });
+ }
+ }
+
+ var err_to_show = errors.slice(-1)[0]; // take the last error to show
+ $_hint.attr("class", err_to_show.cls).text(err_to_show.text);
},
update_button: function (mode, enabled) {
@@ -2154,18 +2629,34 @@ $.extend(wot, { ratingwindow: {
},
set_comment: function (text) {
- $("#user-comment").val(text);
+ $("#user-comment")[0].textContent = text; // different in Firefox
},
has_valid_comment: function () {
- var comment = $("#user-comment").val().trim(),
- _this = wot.ratingwindow.comments;
-
- return (comment.length >= _this.MIN_LIMIT && comment.length < _this.MAX_LIMIT);
+ var _this = wot.ratingwindow.comments,
+ _wg = wot.ratingwindow.wg,
+ is_wgcommenting = wot.ratingwindow.modes.is_current("wgcomment") || wot.ratingwindow.is_wg_allowed,
+ comment = _this.get_comment_value(),
+ minlen = _this.get_minlen(is_wgcommenting),
+ maxlen = _this.get_maxlen(is_wgcommenting);
+
+ if (is_wgcommenting) {
+ var tags = _wg.get_tags(comment);
+
+ return (comment.length >= minlen &&
+ comment.length < maxlen &&
+ tags.length >= _this.MIN_TAGS &&
+ tags.length <= _this.MAX_TAGS);
+
+ } else {
+ return (comment.length >= minlen && comment.length < maxlen);
+ }
},
focus: function () {
- $("#user-comment").focus();
+ setTimeout(function(){
+ $("#user-comment").get(0).focus();
+ }, 200);
},
show_normal_hint: function () {
@@ -2185,7 +2676,458 @@ $.extend(wot, { ratingwindow: {
$("#comment-side-hint").hide();
$("#user-comment").addClass("warning").attr("disabled", "1");
$("#comment-captcha").show();
- }
- }
+ },
+
+ update_caret: function (event, element) {
+ var _this = wot.ratingwindow.comments,
+ sel = window.getSelection(),
+ range = sel.getRangeAt(0);
+
+ if (!range) {
+ _this.caret_top = null;
+ _this.caret_left = null;
+ return;
+ }
+
+ var cr = range.getClientRects();
+
+ if (!cr || !cr[0] || cr[0].width !== 0) { // width == 0 means there is no selected text but only caret position
+ _this.caret_top = null;
+ _this.caret_left = null;
+ return;
+ }
+
+ _this.caret_left = cr[0].left;
+ _this.caret_top = cr[0].top;// - parent.offsetTop + b.top;
+ _this.caret_bottom = cr[0].bottom;// - parent.offsetTop + b.top;
+ },
+
+ tags: {
+ }
+ },
+
+ wg: { // WOT Groups functionality
+
+ init_handlers: function () {
+ var rw = wot.ratingwindow,
+ _this = rw.wg;
+
+ $(document).on("click", ".wg-tag", _this.navigate_tag);
+
+ $("#wg-change, #wg-addmore").on("click", function (e) {
+ rw.modes.wgcomment.activate();
+ });
+
+ $("#wg-about").on("click", function (e) {
+ rw.modes.wg_about.activate();
+ });
+
+ $("#wg-about-ok").on("click", function (e) {
+ rw.modes.auto();
+ });
+
+ $("#wg-about-learnmore").on("click", function (e) {
+ rw.navigate(wot.urls.wg_about, wot.urls.contexts.wg_about_learnmore)
+ });
+
+ $("#wg-expander")
+ .on("click", function () {
+ var $this = $(this);
+
+ if (wot.ratingwindow.modes.current_mode != "wgexpanded") {
+ $this.data("prev-mode", wot.ratingwindow.modes.current_mode);
+ rw.modes.wgexpanded.activate();
+ } else {
+ rw.modes[$this.data("prev-mode")].activate(true);
+ }
+
+ });
+
+ $(document).on("mouseenter", ".wg-tag.info", _this.on_info_tag_hover);
+ $(document).on("mouseleave", ".wg-tag.info", _this.on_info_tag_leave);
+ $(document).on("mouseenter", "#wg-viewer", _this.on_wgviewer_hover);
+ $(document).on("mouseleave", "#wg-viewer", _this.on_wgviewer_leave);
+
+ },
+
+ get_tags: function (text) {
+ var _comments = wot.ratingwindow.comments;
+ text = text ? text : _comments.get_comment_value();
+
+ return wot.tags.get_tags(text);
+ },
+
+ get_all_my_tags: function () {
+ // returns all user's tags
+
+ var rw = wot.ratingwindow,
+ bgwot = rw.get_bg("wot");
+
+ return bgwot.core.tags.mytags;
+ },
+
+ get_popular_tags: function () {
+
+ var rw = wot.ratingwindow,
+ bgwot = rw.get_bg("wot");
+
+ return bgwot.core.tags.popular_tags;
+ },
+
+ update_mytags: function (force) {
+ var rw = wot.ratingwindow,
+ bgwot = rw.get_bg("wot");
+
+ if (!force &&
+ bgwot.core.tags.mytags_updated !== null &&
+ bgwot.core.tags.MYTAGS_UPD_INTERVAL + bgwot.core.tags.mytags_updated > Date.now()) {
+ return false;
+ }
+
+ bgwot.api.tags.my.get_tags();
+ },
+
+ update_popular_tags: function (force) {
+ var rw = wot.ratingwindow,
+ bgwot = rw.get_bg("wot");
+
+ if (!force &&
+ bgwot.core.tags.popular_tags_updated !== null &&
+ bgwot.core.tags.POPULARTAGS_UPD_INTERVAL + bgwot.core.tags.popular_tags_updated > Date.now()) {
+ return false;
+ }
+
+ bgwot.api.tags.popular.get_tags();
+ },
+
+ is_group: function (tag) {
+ var _this = wot.ratingwindow.wg,
+ popular_groups = _this.get_popular_tags(),
+ res = popular_groups.filter(function(item){
+ if (item.value == tag) return true;
+ });
+
+ return res.length > 0;
+ },
+
+ is_mytag: function (tag) {
+ var _this = wot.ratingwindow.wg,
+ tags = _this.get_tags(),
+ res = tags.filter(function(item){
+ if (item.value == tag) return true;
+ });
+
+ return res.length > 0;
+ },
+
+ get_info: function (tag_value) {
+ var infos = {
+ "drupal": {
+ info: "http://en.m.wikipedia.org/wiki/Drupal"
+ },
+ "programming": {
+ info: "http://en.m.wikipedia.org/wiki/Computer_programming"
+ },
+ "finland": {
+ info: "http://en.m.wikipedia.org/wiki/Finland"
+ },
+ "php": {
+ info: "http://en.m.wikipedia.org/wiki/Php"
+ },
+ "javascript": {
+ info: "http://en.m.wikipedia.org/wiki/Javascript"
+ },
+ "jquery": {
+ info: "http://en.m.wikipedia.org/wiki/Jquery"
+ },
+ "opensource": {
+ info: "http://en.m.wikipedia.org/wiki/Opensource"
+ },
+ "html": {
+ info: "http://en.m.wikipedia.org/wiki/Html"
+ },
+ "ransomeware": {
+ info: "http://en.m.wikipedia.org/wiki/Ransomware_(malware)"
+ },
+ "lapsi": {
+ info: "http://en.m.wikipedia.org/wiki/Lapsi"
+ },
+ "cycling": {
+ info: "http://en.m.wikipedia.org/wiki/Cycling"
+ }
+
+ };
+
+ var ltag = tag_value ? tag_value.trim().toLowerCase() : null;
+
+ if (ltag) {
+ return infos[ltag];
+ }
+
+ return null;
+ },
+
+ navigate_tag: function (e) {
+ var _rw = wot.ratingwindow,
+ tag_text = $(this).text();
+
+ _rw.navigate(wot.urls.wg + "?query=" + tag_text, wot.urls.contexts.wg_tag);
+ },
+
+ on_info_tag_hover: function (e) {
+
+ var rw = wot.ratingwindow;
+
+ if (rw.wg_viewer_timer) {
+ window.clearTimeout(rw.wg_viewer_timer);
+ }
+
+ if (rw.wg_infotag_timer) window.clearTimeout(rw.wg_infotag_timer);
+
+ var $this = $(this);
+
+ rw.wg_infotag_timer = window.setTimeout(function() {
+ var $wgviewer = $("#wg-viewer"), $viewer_frame = $("#wg-viewer-frame");
+ var info = $this.data("wg-info");
+
+ $viewer_frame.attr("src", info);
+
+ $wgviewer.show();
+
+ $viewer_frame
+ .toggleClass("mini", !$viewer_frame.hasClass("shown"))
+ .show({ duration: 0, complete: function () {
+ setTimeout(function (){
+ $viewer_frame
+ .removeClass("mini")
+ .addClass("shown");
+
+ }, 200);
+ } });
+ }, 1000); // wait a bit to avoid unnecessary showing
+
+ },
+
+ on_info_tag_leave: function () {
+
+ var rw = wot.ratingwindow;
+
+ if (rw.wg_viewer_timer) {
+ window.clearTimeout(rw.wg_viewer_timer);
+ }
+
+ if (rw.wg_infotag_timer) window.clearTimeout(rw.wg_infotag_timer);
+
+ rw.wg_viewer_timer = window.setTimeout(function (){
+ $("#wg-viewer").hide();
+ $("#wg-viewer-frame").removeClass("mini shown");
+ }, 300);
+ },
+
+ on_wgviewer_hover: function () {
+ if (wot.ratingwindow.wg_viewer_timer) {
+ window.clearTimeout(wot.ratingwindow.wg_viewer_timer);
+ }
+ },
+
+ on_wgviewer_leave: function () {
+ if (wot.ratingwindow.wg_viewer_timer) {
+ window.clearTimeout(wot.ratingwindow.wg_viewer_timer);
+ }
+
+ wot.ratingwindow.wg_viewer_timer = window.setTimeout(function (){
+ $("#wg-viewer").hide();
+ $("#wg-viewer-frame").removeClass("mini shown");
+ }, 300);
+ },
+
+ update_wg_visibility: function () {
+ var _this = wot.ratingwindow,
+ $rw = $("#wot-ratingwindow"),
+ $wg_area = $("#wg-area");
+
+ $rw.toggleClass("wg", _this.is_wg_allowed);
+
+ if (_this.is_wg_allowed) {
+ var visible = !$rw.hasClass("commenting") && !$rw.hasClass("thanks") && !$rw.hasClass("rate");
+ $wg_area.toggle(visible);
+
+ // reset traces of WGEXPANDED mode
+ if (!_this.modes.is_current("wgexpanded")) {
+ wot.ratingwindow.modes.wgexpanded.reset();
+ }
+
+ } else {
+ $wg_area.hide();
+ }
+ },
+
+ suggest_tags: function () {
+ // autocomplete feature for WG comment
+ // TODO: enable only if WG is enabled
+
+ var rw = wot.ratingwindow,
+ _wg = rw.wg,
+ tags_ac = [];
+
+ if (rw.is_wg_allowed) {
+
+ var mytags = _wg.get_all_my_tags(),
+ popular_tags = _wg.get_popular_tags();
+
+ // make a tag list from tags assigned to the website
+ tags_ac = rw.tags.map(function(item){
+ return item.value;
+ });
+
+ // add all user's tags if they are not in the list yet
+ tags_ac = tags_ac
+ .concat(
+ mytags
+ .map(function(item){
+ return item.value;
+ })
+ .filter(function (el, index, arr) {
+ return (tags_ac.indexOf(el) < 0);
+ }));
+
+ // then add all popular tags if they are not in the list yet (this is why this concat is separated from above)
+ tags_ac = tags_ac
+ .concat(
+ popular_tags
+ .map(function(item){
+ return item.value;
+ })
+ .filter(function (el, index, arr) {
+ return (tags_ac.indexOf(el) < 0);
+ })
+ );
+
+ tags_ac.sort();
+
+ tags_ac = tags_ac.map(function (item) { return "#" + item }); // prepend with # char since it is required by the autocomplete feature
+
+// console.log(tags_ac);
+ }
+
+ return tags_ac;
+ },
+
+ update_wg_tags: function () {
+ var rw = wot.ratingwindow,
+ _wg = rw.wg,
+ mytags = _wg.get_tags(), // get list of user tags for the current website
+ $tags = $("#wg-tags"),
+ tagmap = [
+ { list: mytags },
+ { list: rw.tags }
+ ],
+ has_tags = 0,
+ prev = {};
+
+ $tags.empty(); // clean the tags' section
+
+ for (var i = 0; i < tagmap.length; i++) {
+ var list = tagmap[i].list;
+
+ for (var j = 0; j < list.length; j++) {
+
+ var $tag, info,
+ tag = list[j],
+ tag_value = tag.value;
+
+ if (prev[tag_value]) continue; // don't show one tag more than one time (if it was in mytags list)
+
+ $tag = $("<li></li>")
+ .addClass("wg-tag")
+ .toggleClass("group", _wg.is_group(tag_value))
+ .toggleClass("mytag", _wg.is_mytag(tag_value));
+
+ info = _wg.get_info(tag_value);
+
+ if (info) {
+ // this is a group with additional info linked
+ var $tag_inner = $("<span></span>");
+ $tag_inner.text(tag_value);
+ $tag
+ .append($tag_inner)
+ .addClass("info")
+ .data("wg-info", info.info);
+
+ } else {
+ $tag.text(tag_value); // this is generic tag/group
+ }
+
+
+ $tags.append($tag);
+ prev[tag_value] = true; // remember that we showed the tag
+ }
+
+ }
+
+ has_tags = $tags.children().length > 0;
+
+ var $wg_edit = $("#wg-change"),
+ $wg_addmore = $("#wg-addmore");
+
+ $wg_edit.text( mytags.length > 0 ? wot.i18n("wg", "edit") : wot.i18n("wg", "add") );
+ $wg_addmore.toggleClass("hidden", has_tags);
+ $tags.toggle(has_tags);
+
+ var e_tags = $tags.get(0);
+ var is_partially = e_tags && e_tags.scrollHeight > e_tags.clientHeight; // test whether there are tags that don't fit
+
+ $("#wg-expander").toggleClass("hidden", !is_partially);
+ },
+
+ show_tagautocomplete: function () {
+
+ var _comments = wot.ratingwindow.comments;
+
+ var top, left,
+ pos = this.$element.position(),
+ area_height = this.$element[0].offsetHeight,
+ area_width = this.$element[0].offsetWidth;
+
+ if (_comments.caret_left == null || _comments.caret_bottom == null) {
+ top = pos.top;
+ left = pos.left;
+ } else {
+ top = _comments.caret_bottom;
+ left = _comments.caret_left + _comments.AUTOCOMPLETE_OFFSET_X;
+
+ // TODO: ajust the position on the edges
+ }
+
+ this.$menu
+ .appendTo('body')
+ .show()
+ .css({
+ position: "absolute",
+ top: "99999px",
+ left: "99999px"
+ });
+
+ // adjust position and avoid going beyond the right and bottom edges of the text area
+ var popup_height = this.$menu.height(),
+ popup_width = this.$menu.width();
+
+ if (left + popup_width > pos.left + area_width) {
+ left = pos.left + area_width - popup_width;
+ }
+
+ if (top + popup_height > pos.top + area_height) {
+ top = _comments.caret_top - popup_height - 20;
+ }
+
+ this.$menu.css({
+ top: top + "px",
+ left: left + "px"
+ });
+
+ this.shown = true;
+ return this;
+ }
+ } // End of wg object
}});
diff --git a/content/rw/wot.js b/content/rw/wot.js
index 2e1cc68..0700338 100644
--- a/content/rw/wot.js
+++ b/content/rw/wot.js
@@ -19,7 +19,7 @@
*/
var wot = {
- version: 20140113,
+ version: 20140217,
platform: "firefox",
locale: "en", // cached value of the locale
lang: "en-US", // cached value of the lang
@@ -34,6 +34,7 @@ var wot = {
is_mailru: false,
is_yandex: false,
is_rambler: false,
+
is_accessible: false
},
@@ -126,6 +127,8 @@ var wot = {
tour: "http://www.mywot.com/support/tour/",
tour_rw: "http://www.mywot.com/support/tour/ratingwindow",
tour_scorecard: "http://www.mywot.com/support/tour/scorecard",
+ wg: "https://dev.mywot.com/en/groups/g",
+ wg_about: "https://dev.mywot.com/en/groups",
contexts: {
rwlogo: "rw-logo",
@@ -148,7 +151,9 @@ var wot = {
wt_warn_lm: "wt-warn-lm",
wt_warn_logo: "wt-warn-logo",
wt_donuts_lm: "wt-donuts-lm",
- wt_donuts_logo: "wt-donuts-logo"
+ wt_donuts_logo: "wt-donuts-logo",
+ wg_tag: "wg-tag",
+ wg_about_learnmore: "wg-learnmore"
}
},
@@ -231,6 +236,8 @@ var wot = {
expire_warned_after: 20000, // number of milliseconds after which warned flag will be expired
+ TINY_THANKYOU_DURING: 60 * 60 * 1000, // within this time after prev rating user won't see separate ThankYou screen after new submission. Milliseconds.
+
// trusted extensions IDs
allowed_senders: {
"ihcnfeknmfflffeebijjfbhkmeehcihn": true, // dev version
@@ -934,7 +941,36 @@ var wot = {
}
return false;
- }
+ },
+
+ tags: {
+ tags_re: /(\s|^)#([a-zä-ö0-9\u0400-\u04FF]{2,})/img, // Also change the regexp at content/wg.js
+ tags_validate_re: /^\d{2}$/im,
+
+ get_tags: function (text) {
+
+ if (!text) return [];
+
+ var res,
+ tags = [],
+ _tags = {};
+
+ while ((res = wot.tags.tags_re.exec(text)) !== null) {
+ var tag = res[2] || "";
+
+ if (wot.tags.tags_validate_re.test(tag)) continue; // skip invalid tag
+
+ if (tag && !_tags[tag]) {
+ tags.push({
+ value: tag // tag's text
+ });
+ _tags[tag] = true; // remember the tag to avoid duplications
+ }
+ }
+ wot.tags.tags_re.lastIndex = 0; // reset the last index to avoid using it for the different text
+ return tags;
+ }
+ }
};
@@ -1061,9 +1097,37 @@ wot.utils = {
},
isEmptyObject: function (obj) {
- for (var name in obj) {
- return false;
+ for (var name in obj) {
+ return false;
+ }
+ return true;
+ },
+
+ query_param: function(obj, prefix) {
+ var str = [];
+ for(var p in obj) {
+ var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
+ str.push(typeof v == "object" ?
+ wot.utils.query_param(v, k) :
+ encodeURIComponent(k) + "=" + encodeURIComponent(v));
}
- return true;
+ return str.join("&");
+ },
+
+ getParams: function (query) {
+ var params = {};
+ if (location.search) {
+ var parts = query.split('&');
+
+ parts.forEach(function (part) {
+ var pair = part.split('=');
+ pair[0] = decodeURIComponent(pair[0]);
+ pair[1] = decodeURIComponent(pair[1]);
+ params[pair[0]] = (pair[1] !== 'undefined') ?
+ pair[1] : true;
+ });
+ }
+ return params;
}
+
};
diff --git a/content/tools.js b/content/tools.js
index 663ac02..d110cc3 100644
--- a/content/tools.js
+++ b/content/tools.js
@@ -22,7 +22,15 @@
var wot_modules = [];
var wot_tools = {
+
wdump: function (str) {
dump(str + "\n");
+ },
+
+ log: function() {
+ Array.prototype.slice.call(arguments).forEach(function (item, index, arr) {
+ dump(JSON.stringify(item, null, ' ') + "\n");
+ })
}
+
};
diff --git a/content/wg.js b/content/wg.js
new file mode 100644
index 0000000..9a0a057
--- /dev/null
+++ b/content/wg.js
@@ -0,0 +1,143 @@
+/*
+ wg.js
+ Copyright © 2014 - WOT Services Oy <info at mywot.com>
+
+ This file is part of WOT.
+
+ WOT is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ WOT is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+ License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with WOT. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+var wot_wg = {
+ /* WOT Groups core functionality */
+
+ mytags: [ ],
+ mytags_updated: null, // time when the list was updated last time
+ MYTAGS_UPD_INTERVAL: 30 * 60 * 1000,
+
+ popular_tags: [ ],
+ popular_tags_updated: null,
+ POPULARTAGS_UPD_INTERVAL: 30 * 60 * 1000,
+
+ tags_re: /(\s|^)#([a-zä-ö0-9\u0400-\u04FF]{2,})/img, // Also change the regexp at content/rw/wot.js
+ tags_validate_re: /^\d{2}$/im,
+
+ is_enabled: function () {
+ return (wot_hashtable.get("wg_enabled") == true);
+ },
+
+ enable: function (is_enabled) {
+ wot_hashtable.set("wg_enabled", is_enabled);
+ },
+
+ get_mytags_updated: function () {
+ return wot_hashtable.get("mytags_updated") || null;
+ },
+
+ get_popular_tags_updated: function () {
+ return wot_hashtable.get("popular_tags_updated") || null;
+ },
+
+ get_mytags: function () {
+ var json = wot_hashtable.get("mytags_list") || "[]";
+ return JSON.parse(json);
+ },
+
+ get_popular_tags: function () {
+ var json = wot_hashtable.get("popular_tags_list") || "[]";
+ return JSON.parse(json);
+ },
+
+ set_mytags: function (tags) {
+ // this function is called in dynamic way, e.g. by concatenating string "set_" + variable.
+ tags = tags || [];
+ wot_hashtable.set("mytags_list", JSON.stringify(tags));
+ wot_hashtable.set("mytags_updated", Date.now());
+ },
+
+ set_popular_tags: function (tags) {
+ tags = tags || [];
+ wot_hashtable.set("popular_tags_list", JSON.stringify(tags));
+ wot_hashtable.set("popular_tags_updated", Date.now());
+ },
+
+ update_tags: function () {
+ // Checks whether particular tag list is expired or haven't been updated yet, and fetches the list
+
+ var tmap = [
+ {
+ keyword: "mytags",
+ method: "getmytags",
+ time_func: wot_wg.get_mytags_updated,
+ time: wot_wg.MYTAGS_UPD_INTERVAL
+ }, {
+ keyword: "popular_tags",
+ method: "getmastertags",
+ time_func: wot_wg.get_popular_tags_updated,
+ time: wot_wg.POPULARTAGS_UPD_INTERVAL
+ }
+ ];
+
+ for (var i = 0; i < tmap.length; i++) {
+ var obj = tmap[i];
+ var last_updated = obj.time_func.apply();
+ if (!last_updated || obj.time + last_updated < Date.now()) {
+ wot_api_tags.get_tags(obj.keyword, obj.method);
+ }
+ }
+ },
+
+ append_mytags: function (mytags) {
+ if (mytags instanceof Array && mytags.length) {
+
+ var _this = this,
+ mytags_flat = _this.get_mytags().map(function (item) { return item.value });
+
+ var uniq = mytags.filter(function (tag) {
+ var tag_value = tag.value.trim();
+ return mytags_flat.indexOf(tag_value) < 0;
+ });
+
+ var uniq_tags = uniq.map(function (tag) {
+ tag.mytag = true;
+ return tag;
+ });
+
+ this.set_mytags(_this.mytags.concat(uniq_tags));
+ }
+ },
+
+ extract_tags: function (text) {
+
+ if (!text) return [];
+
+ var res,
+ tags = [],
+ _tags = {};
+
+ while ((res = this.tags_re.exec(text)) !== null) {
+ var tag = res[2] || "";
+ if (this.tags_validate_re.test(tag)) continue; // skip invalid tag
+ if (tag && !_tags[tag]) {
+ tags.push({
+ value: tag // tag's text
+ });
+ _tags[tag] = true; // remember the tag to avoid duplications
+ }
+ }
+ this.tags_re.lastIndex = 0; // reset the last index to avoid using it for the different text
+ return tags;
+ }
+};
+
+wot_modules.push({ name: "wot_wg", obj: wot_wg });
diff --git a/locale/en-US/wot.properties b/locale/en-US/wot.properties
index 536999a..80562f1 100644
--- a/locale/en-US/wot.properties
+++ b/locale/en-US/wot.properties
@@ -141,3 +141,16 @@ ratingwindow_check_3a = Other users may question if all negative statements on c
ratingwindow_check_4a = Other users may not consider your review credible, because you selected several unrelated things.
ratingwindow_check_5a = 'Other' category should be used only if no other options applies, and then explained in written comments.
ratingwindow_check_6a = Too many categories may result in people questioning the credibility of your review.
+wg_title = WOT Groups<span class='wg-beta'>beta</span>
+wg_edit = edit my tags
+wg_add = add a tag
+wg_add_long = click to comment with tags
+wg_expander = show more
+wg_expander_less = show less
+wg_about = what's this?
+wg_viewer_title_wikipedia = Wikipedia
+wg_about_title = What is WOT Groups Beta?
+wg_about_content = WOT Groups Beta enables various communities to build together a perfect Web site online directory around their passion and interests. The filtering and site reputation tools make the Web use easy and trusted.
+wg_about_ok = OK, got it
+wg_about_more = open Groups
+ratingwindow_wgcommenthints = <p>Be precise and truthful</p><p>Use tags as shorthand</p><p>Comment what you know</p>
diff --git a/skin/b/message.png b/skin/b/message.png
new file mode 100755
index 0000000..8092b51
Binary files /dev/null and b/skin/b/message.png differ
diff --git a/skin/ratingwindow.css b/skin/ratingwindow.css
index a197f60..f3c4936 100644
--- a/skin/ratingwindow.css
+++ b/skin/ratingwindow.css
@@ -1,6 +1,6 @@
/*
ratingwindow.css
- Copyright © 2009 - 2013 WOT Services Oy <info at mywot.com>
+ Copyright © 2009 - 2014 WOT Services Oy <info at mywot.com>
This file is part of WOT.
@@ -27,10 +27,35 @@ body {
padding: 0;
}
+.hidden {
+ display: none !important;
+}
+
textarea {
resize: none;
}
+.link {
+ color: #3073c5;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+.pseudo-link {
+ color: #3073c5;
+ cursor: pointer;
+ border-bottom: 1px dotted #3073c5;
+}
+
+.pseudo-link:hover {
+ color: red;
+ border-color: red;
+}
+
#wot-ratingwindow {
display: block;
padding: 10px 16px 0;
@@ -96,7 +121,7 @@ textarea {
/* Always visible */
#main-area {
- min-height: 182px;
+ min-height: 184px;
padding: 4px 10px;
margin: 0 -11px;
}
@@ -105,6 +130,11 @@ textarea {
min-height: 210px;
}
+.wg.view-mode #main-area,
+.wg.wgcommenting #main-area {
+ min-height: 165px;
+}
+
/* Always visible */
#bottom-area {
/*min-height: 3em;*/
@@ -137,6 +167,63 @@ textarea {
width: 42px;
}
+ at -moz-keyframes flashing {
+ 0% { opacity: 0.1; }
+ 50% { opacity: 0.9; }
+ 100% { opacity: 0.1; }
+}
+
+#message-indicator {
+ background-size: 16px auto;
+ width: 16px;
+ height: 16px;
+ display: table-cell;
+ background-position-y: -2px;
+ padding: 0 0.6em;
+ background-repeat: no-repeat;
+ background-image: url("chrome://wot/skin/b/message.png");
+ background-position: 0 4px; /* diff in FF */
+ visibility: hidden;
+}
+
+#message-indicator.unseen {
+ -moz-animation: flashing 0.8s linear infinite;
+ visibility: visible;
+}
+
+#message-indicator.unseen:hover {
+ -moz-animation: none;
+ opacity: 1;
+}
+
+#message-indicator.seen {
+ opacity: 0.1;
+ visibility: visible;
+}
+
+#message-indicator.seen:hover {
+ opacity: 1;
+}
+
+#floating-message {
+ display: none;
+ position: absolute;
+ top: 40px;
+ left: 150px;
+ width: 362px;
+ z-index: 5;
+ background-color: #F5F5F5;
+ padding: 10px 15px;
+ border: 1px solid #C0C0C0;
+ border-radius: 3px;
+ box-shadow: 1px 1px 4px 1px rgba(94, 94, 94, 0.48);
+}
+
+#floating-message-text {
+ font-size: 13px;
+ line-height: 1.5em;
+}
+
#wot-header-links {
position: absolute;
top: 2px;
@@ -156,6 +243,7 @@ textarea {
.wot-header-link:hover {
color: #3073c5;
cursor: pointer;
+ text-decoration: underline;
}
.unregistered #wot-header-link-profile {
@@ -234,6 +322,18 @@ textarea {
/*color: #4e4e4e;*/
/*}*/
+#tiny-thankyou {
+ display: none;
+ position: absolute;
+ margin: -35px 0 0 150px;
+ padding: 0.6em 1.5em;
+ background-color: rgba(195, 241, 173, 0.8);
+ color: #000;
+ font-size: 13px;
+ z-index: 25;
+ border-radius: 3px;
+}
+
/* rating header */
#wot-rating-header {
color: #878787;
@@ -254,13 +354,17 @@ textarea {
left: 14px;
}
-#myrating-header {
+.title {
margin-left: 10px;
font-size: 14px;
font-weight: bold;
color: #454545;
}
+#myrating-header {
+
+}
+
/* Headers for rating controls */
#wot-myrating-0-header,
#wot-myrating-4-header {
@@ -347,6 +451,10 @@ textarea {
height: 20px;
}
+#rated-votes.voted {
+ height: 24px; /* fixing a bug when mode is switched from wgcomment to wgexpanded */
+}
+
#rated-votes.voted #voted-categories {
color: #6A6A6A;
}
@@ -463,9 +571,6 @@ textarea {
position: absolute;
right: 50px;
margin-top: 5px;
- border-bottom: 1px dotted #3163B9;
- color: #3163B9;
- cursor: pointer;
}
/* don't chow this link during "commenting" mode */
@@ -628,38 +733,26 @@ textarea {
}
#wot-rating-header-wot {
- display: block;
- font-size: 14px;
- color: #454545;
- text-align: left;
+ display: inline-block;
+ width: 264px;
margin-top: 8px;
- font-weight: bold;
- margin-left: 11px;
+ text-align: left;
}
#rep-block {
/*width: 200px;*/
}
-/* scorecard */
-#wot-scorecard {
- position: absolute;
- right: 104px;
- top: 130px;
- z-index: 3;
-}
-
#wot-scorecard-content {
+ display: inline-block;
+ max-width: 250px;
+ max-height: 1.3em; /* Diff in FF */
+ overflow: hidden;
}
.wot-scorecard-text {
- color: #3073c5;
font-size: 12px;
- cursor: pointer;
-}
-
-.wot-scorecard-text:hover {
- text-decoration: underline;
+ font-weight: bold;
}
#wot-scorecard-visit {
@@ -691,7 +784,7 @@ textarea {
#tr-categories-list {
margin: 11px 0 0 0;
padding: 0;
- min-height: 46px;
+ min-height: 50px;
line-height: 9px;
column-count: 2;
-moz-column-count: 2;
@@ -846,6 +939,10 @@ textarea {
display: none;
}
+.wg #user-communication {
+ display: none !important;
+}
+
.user-comm-activity {
margin: 77px 25px 0;
position: relative;
@@ -930,13 +1027,14 @@ textarea {
height: auto;
line-height: 13px;
overflow: hidden;
- padding: 8px 20px 2px;
+ padding: 8px 20px 8px;
text-align: center;
white-space: normal;
}
#wot-message-text[url^="http"]:hover,
#wot-message[status="important"] #wot-message-text[url^="http"]:hover,
-#wot-message[status="critical"] #wot-message-text[url^="http"]:hover {
+#wot-message[status="critical"] #wot-message-text[url^="http"]:hover,
+#floating-message[url^="http"]:hover {
color: #3073c5;
cursor: pointer;
}
@@ -953,6 +1051,291 @@ textarea {
#wot-message[status="critical"] #wot-message-text {
color: #d81f27;
}
+
+#floating-message[status="important"] {
+ background-color: #FFD5AB;
+}
+
+#floating-message[status="critical"] {
+ background-color: #FF8B90;
+}
+
+/* -- Web Guide */
+
+#wg-area {
+ display: none;
+ min-height: 65px;
+}
+
+#wg-area:before {
+ position: absolute;
+ height: 0;
+ border-top: 1px solid #D9D9D9;
+ content: "";
+ width: 100%;
+ margin-top: 0px;
+ margin-left: -24px;
+}
+
+.wgexpanded #wg-area:before {
+ margin-top: -1px; /* hide the top line to avoid doubling of the separator */
+}
+
+#wg-about-area {
+ display: none;
+ padding: 0px 20px;
+ font-size: 13px;
+}
+
+#wg-about-area p {
+ line-height: 1.5em;
+}
+
+.text-title {
+ font-weight: bold;
+}
+
+#wg-about-learnmore {
+ display: inline-block;
+ position: absolute;
+ right: 50px;
+}
+
+#wg-about-ok {
+ display: inline-block;
+}
+
+.wg-title-bar {
+ width: 274px;
+ display: block;
+ margin-bottom: 3px;
+}
+
+#wg-title {
+ display: inline-block;
+ margin-top: 11px;
+ margin-bottom: 2px;
+}
+
+.wg-beta {
+ vertical-align: super;
+ font-size: 9px;
+ padding-left: 4px;
+ color: #5C5C5C;
+}
+
+#wg-right-bar {
+ position: absolute;
+ right: 33px;
+ margin-top: -25px;
+}
+
+#wg-right-bar * {
+ -moz-user-select: none;
+}
+
+#wg-change {
+ display: inline-block;
+ font-size: 12px;
+ margin: 0 4px;
+ font-variant: small-caps;
+ border: 1px solid rgba(49, 99, 185, 0.7); /* diff in FF */
+ color: #3163B9;
+ cursor: pointer;
+ padding: 1px 12px;
+ border-radius: 9px;
+ font-weight: bold;
+ transition: background-color 0.1s, color 0.1s;
+}
+
+#wg-change:hover {
+ color: #FFF;
+ background-color: rgba(49, 99, 185, 0.94);
+ border: 1px solid rgba(255, 255, 255, 0.32);
+}
+
+#wg-expander {
+ display: inline-block;
+ font-size: 11px;
+ padding: 0px 0px;
+ margin: 0 4px;
+}
+
+#wg-expander:hover {
+ /*color: #FFF;*/
+ /*background-color: rgba(49, 99, 185, 0.94);*/
+ /*border-bottom: 1px solid rgba(255, 255, 255, 0.32);*/
+}
+
+.wgcommenting #wg-change {
+ display: none;
+}
+
+#wg-about {
+ display: inline-block;
+ font-size: 11px;
+ margin: 0 4px;
+ border-bottom: 1px dotted #E2E2E2;
+ color: #B8B8B8;
+ cursor: pointer;
+}
+
+#wg-about:hover {
+ border-bottom: 1px dotted #3163B9;
+ color: #3163B9;
+}
+
+#wg-tags {
+ margin-left: 2px;
+ margin-top: -1px; /* Diff in FF */
+ height: 1.6em; /* diff in FF */
+ max-height: none;
+ overflow: hidden;
+ -moz-margin-before: 0;
+ -moz-margin-after: 0;
+ -moz-padding-start: 0;
+ list-style-type: none;
+}
+
+#wg-tags.expanded {
+ height: auto;
+ max-height: 190px;
+}
+
+/*#wg-my-tags {*/
+ /*display: inline-block;*/
+/*}*/
+
+/*#wg-other-tags {*/
+ /*display: inline-block;*/
+ /*-webkit-margin-before: 0;*/
+ /*-webkit-margin-after: 0;*/
+ /*-webkit-padding-start: 0;*/
+ /*list-style-type: none;*/
+/*}*/
+
+.wg-tag {
+ display: inline-block;
+ font-size: 14px;
+ padding: 2px 4px;
+ margin-right: 2px;
+ margin-left: 2px;
+ margin-bottom: 3px;
+ line-height: 1.4em;
+ color: #5a5450;
+ text-decoration: none;
+ cursor: pointer;
+ transition: border-color 0.2s, color 0.2s;
+}
+
+.wg-tag:not(.group):hover {
+ text-decoration: underline;
+}
+
+.wg-tag.group {
+ font-size: 14px;
+ font-weight: bold;
+ border: 1px solid transparent;
+ padding: 2px 6px;
+ border-radius: 10px;
+}
+
+.wg-tag.mytag {
+ font-size: 12px;
+}
+
+.wg-tag.mytag:not(.group):hover:before {
+ content: "#";
+ color: #757575;
+ margin-left: -7px;
+ position: absolute;
+ font-size: 10px;
+ font-weight: lighter;
+ margin-top: 2px;
+}
+
+.wg-tag.group:hover {
+ border-color: rgba(68, 137, 255, 0.5);;
+ color: #3163B9;
+}
+
+.wg-tag.info:after {
+ position: relative;
+ top: -5px;
+ left: 1px;
+ line-height: 1.2em;
+ display: inline-block;
+ transition: background-color 0.2s;
+ content: "W";
+ font-family: serif;
+ background-color: #B3B3B3;
+ font-size: 9px;
+ color: #FFF;
+ /*padding: 1px 2px;*/
+ padding: 2px 2px 0; /* Diff in FF */
+ border-radius: 4px;
+}
+
+.wg-tag.info:hover:after {
+ background-color: #171717;
+}
+
+#wg-addmore {
+ display: inline-block;
+ color: #C0C0C0;
+ font-style: italic;
+ font-size: 12px;
+ margin: 5px 10px;
+ cursor: pointer;
+ transition: border 0.2s, color 0.2s;
+}
+
+#wg-addmore:hover {
+ border-bottom: 1px dotted #898989;
+ color: #898989;
+}
+
+#wg-viewer {
+ position: absolute;
+ top: 30px;
+ left: 66px;
+ width: 420px;
+ height: 330px;
+ border: 1px solid #C0C0C0;
+ box-shadow: 2px 2px 11px rgba(73, 73, 73, 0.48);
+ border-radius: 3px;
+ background-color: #FFF;
+ z-index: 60;
+ transition: opacity 0.5s;
+ padding-bottom: 10px;
+}
+
+#wg-viewer-frame.mini { /* pre-loading style to compensate flickering of the content inside iframe */
+ opacity: 0.01;
+}
+
+.wg-viewer-title {
+ height: 20px;
+ padding: 7px 10px 3px 10px;
+ background-color: #D7D7DB;
+ color: #494949;
+}
+
+#wg-viewer-frame {
+ border: none;
+ width: 400px;
+ height: 305px;
+ margin: 0 10px;
+ transition: opacity 0.5s;
+}
+
+.wgcommenting #wg-addmore {
+ visibility: hidden;
+ width: 0; /* hacky way of hiding the element */
+}
+
+/* -- // -- */
+
/* partner */
#wot-partner {
display: none;
@@ -989,6 +1372,15 @@ textarea {
display: none;
}
+.wgcommenting #rate-buttons {
+/* buttons in WebGuide commenting mode are positioned differently */
+ bottom: 90px;
+}
+
+.wgcommenting #btn-comment {
+ display: none !important; /* hide this redundant button */
+}
+
.buttons-wrapper {
position: relative;
}
@@ -1072,6 +1464,10 @@ textarea {
display: block;
}
+.wgcommenting #btn-delete {
+ display: none !important;
+}
+
.btn-submit {
padding-left: 2em;
padding-right: 2em;
@@ -1396,6 +1792,21 @@ input[type=checkbox].css-checkbox:checked + label.css-label {
display: none; /* is shown from JS when needed */
margin: auto 19px;
min-height: 225px;
+ -moz-user-select: none; /* FF specific */
+}
+
+.wgcommenting #commenting-area {
+ min-height: 275px;
+}
+
+.wgcommenting #commenting-area:before {
+ position: absolute;
+ height: 0;
+ border-top: 1px solid #D9D9D9;
+ content: "";
+ width: 100%;
+ margin-top: -9px;
+ margin-left: -34px;
}
.comment-title {
@@ -1403,17 +1814,32 @@ input[type=checkbox].css-checkbox:checked + label.css-label {
padding: 3px 0 4px;
}
+#comment-top-hint {
+ display: none;
+ color: #979797;
+ font-size: 12px;
+ margin-bottom: 15px;
+ line-height: 1.5em;
+}
+
+.wgcommenting #comment-top-hint {
+ display: block;
+}
+
.user-comment-wrapper {
float: right;
}
#user-comment {
+ -moz-appearance: textfield-multiline; /* different in Firefox */
+ -moz-user-select: text; /* different in Firefox */
+ cursor: text; /* different in Firefox */
height: 125px; /* different in Firefox */
width: 350px;
/*margin-right: 10px;*/
padding: 8px;
- border: 1px solid #C0C0C0;
- border-radius: 3px;
+ /*border: 1px solid #C0C0C0;*/
+ /*border-radius: 3px;*/
font-size: 13px;
font-family: Arial;
overflow-x: hidden;
@@ -1462,22 +1888,15 @@ input[type=checkbox].css-checkbox:checked + label.css-label {
#comment-register-link,
#comment-captcha-link {
- color: #3073c5;
font-size: 12px;
- cursor: pointer;
-}
-
-#comment-register-link:hover,
-#comment-captcha-link:hover {
- text-decoration: underline;
}
#comment-bottom-hint {
font-size: 14px;
text-align: right;
position: absolute;
- bottom: 66px;
- right: 56px;
+ bottom: 60px;
+ right: 50px;
background-color: #FC4B56;
padding: 3px 10px;
font-weight: normal;
@@ -1490,6 +1909,10 @@ input[type=checkbox].css-checkbox:checked + label.css-label {
display: block;
}
+.wgcommenting #comment-bottom-hint {
+ bottom: 142px;
+}
+
#thanks-area {
display: none;
min-height: 194px;
@@ -1519,3 +1942,8 @@ input[type=checkbox].css-checkbox:checked + label.css-label {
margin-top: 0.5em;
color: #808080;
}
+
+.typeahead {
+ font-family: "Arial", sans-serif;
+ font-size: 12px;
+}
diff --git a/skin/typeahead.css b/skin/typeahead.css
new file mode 100644
index 0000000..967c464
--- /dev/null
+++ b/skin/typeahead.css
@@ -0,0 +1,238 @@
+/*!
+ * Bootstrap v2.3.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+
+/* This is a part of original Bootstrap 2.3.1 css file */
+
+.input-append .typeahead.dropdown-menu,
+.input-prepend .typeahead.dropdown-menu,
+.input-append .popover,
+.input-prepend .popover {
+ font-size: 14px;
+}
+
+.typeahead.dropdown-menu > li > a:hover > [class^="icon-"],
+.typeahead.dropdown-menu > li > a:focus > [class^="icon-"],
+.typeahead.dropdown-menu > li > a:hover > [class*=" icon-"],
+.typeahead.dropdown-menu > li > a:focus > [class*=" icon-"],
+.typeahead.dropdown-menu > .active > a > [class^="icon-"],
+.typeahead.dropdown-menu > .active > a > [class*=" icon-"],
+.dropdown-submenu:hover > a > [class^="icon-"],
+.dropdown-submenu:focus > a > [class^="icon-"],
+.dropdown-submenu:hover > a > [class*=" icon-"],
+.dropdown-submenu:focus > a > [class*=" icon-"] {
+ background-image: url("../img/glyphicons-halflings-white.png");
+}
+
+.typeahead.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 100px;
+ max-width: 200px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ background-color: #ffffff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ *border-right-width: 2px;
+ *border-bottom-width: 2px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 2px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+}
+
+.typeahead.dropdown-menu.pull-right {
+ right: 0;
+ left: auto;
+}
+
+.typeahead.dropdown-menu .divider {
+ *width: 100%;
+ height: 1px;
+ margin: 9px 1px;
+ *margin: -5px 0 5px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+}
+
+.typeahead.dropdown-menu > li > a {
+ display: block;
+ padding: 4px 10px;
+ margin: 0 2px;
+ clear: both;
+ font-weight: normal;
+ line-height: 12px;
+ color: #333333;
+ white-space: nowrap;
+ text-decoration: none;
+ letter-spacing: 1px;
+ overflow: hidden;
+}
+
+.typeahead.dropdown-menu > li > a:hover,
+.typeahead.dropdown-menu > li > a:focus,
+.typeahead.dropdown-submenu:hover > a,
+.typeahead.dropdown-submenu:focus > a {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: rgba(225, 225, 225, 0.88);
+ background-image: -moz-linear-gradient(top, #e1e1e1, #898989);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#e1e1e1), to(#898989));
+ background-image: -webkit-linear-gradient(top, #e1e1e1, #898989);
+ background-image: -o-linear-gradient(top, #e1e1e1, #898989);
+ background-image: linear-gradient(to bottom, #e1e1e1, #898989);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+
+.typeahead.dropdown-menu > .active > a,
+.typeahead.dropdown-menu > .active > a:hover,
+.typeahead.dropdown-menu > .active > a:focus {
+ color: #ffffff;
+ text-decoration: none;
+ /*background-color: #0081c2;*/
+ /*background-image: -moz-linear-gradient(top, #0088cc, #0077b3);*/
+ /*background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));*/
+ /*background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);*/
+ /*background-image: -o-linear-gradient(top, #0088cc, #0077b3);*/
+ /*background-image: linear-gradient(to bottom, #0088cc, #0077b3);*/
+ background-color: rgba(225, 225, 225, 0.88);
+ background-image: -moz-linear-gradient(top, #e1e1e1, #898989);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#e1e1e1), to(#898989));
+ background-image: -webkit-linear-gradient(top, #e1e1e1, #898989);
+ background-image: -o-linear-gradient(top, #e1e1e1, #898989);
+ background-image: linear-gradient(to bottom, #e1e1e1, #898989); background-repeat: repeat-x;
+ outline: 0;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+
+.typeahead.dropdown-menu > .disabled > a,
+.typeahead.dropdown-menu > .disabled > a:hover,
+.typeahead.dropdown-menu > .disabled > a:focus {
+ color: #999999;
+}
+
+.typeahead.dropdown-menu > .disabled > a:hover,
+.typeahead.dropdown-menu > .disabled > a:focus {
+ text-decoration: none;
+ cursor: default;
+ background-color: transparent;
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.open {
+ *z-index: 1000;
+}
+
+.open > .typeahead.dropdown-menu {
+ display: block;
+}
+
+.pull-right > .typeahead.dropdown-menu {
+ right: 0;
+ left: auto;
+}
+
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid #000000;
+ content: "";
+}
+
+.dropup .typeahead.dropdown-menu,
+.navbar-fixed-bottom .dropdown .typeahead.dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px;
+}
+
+.dropdown-submenu {
+ position: relative;
+}
+
+.dropdown-submenu > .dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+ -webkit-border-radius: 0 6px 6px 6px;
+ -moz-border-radius: 0 6px 6px 6px;
+ border-radius: 0 6px 6px 6px;
+}
+
+.dropdown-submenu:hover > .dropdown-menu {
+ display: block;
+}
+
+.dropup .dropdown-submenu > .dropdown-menu {
+ top: auto;
+ bottom: 0;
+ margin-top: 0;
+ margin-bottom: -2px;
+ -webkit-border-radius: 5px 5px 5px 0;
+ -moz-border-radius: 5px 5px 5px 0;
+ border-radius: 5px 5px 5px 0;
+}
+
+.dropdown-submenu > a:after {
+ display: block;
+ float: right;
+ width: 0;
+ height: 0;
+ margin-top: 5px;
+ margin-right: -10px;
+ border-color: transparent;
+ border-left-color: #cccccc;
+ border-style: solid;
+ border-width: 5px 0 5px 5px;
+ content: " ";
+}
+
+.dropdown-submenu:hover > a:after {
+ border-left-color: #ffffff;
+}
+
+.dropdown-submenu.pull-left {
+ float: none;
+}
+
+.dropdown-submenu.pull-left > .dropdown-menu {
+ left: -100%;
+ margin-left: 10px;
+ -webkit-border-radius: 6px 0 6px 6px;
+ -moz-border-radius: 6px 0 6px 6px;
+ border-radius: 6px 0 6px 6px;
+}
+
+.dropdown .typeahead.dropdown-menu .nav-header {
+ padding-right: 20px;
+ padding-left: 20px;
+}
+
+.typeahead {
+ z-index: 1051;
+ margin-top: 2px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/wot.git
More information about the Pkg-mozext-commits
mailing list