[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