From c41799dd5847392d96f57a87892ff9ac74701921 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 15 Nov 2012 18:49:02 +0100 Subject: [PATCH 001/157] First version, not working already bzr revid: nicolas.vanhoren@openerp.com-20121115174902-utga3vfpozc42fde --- addons/web_chat/__init__.py | 2 + addons/web_chat/__openerp__.py | 19 ++++ addons/web_chat/chat.py | 117 ++++++++++++++++++++++++ addons/web_chat/static/src/css/chat.css | 6 ++ addons/web_chat/static/src/js/chat.js | 46 ++++++++++ addons/web_chat/static/src/xml/chat.xml | 11 +++ 6 files changed, 201 insertions(+) create mode 100644 addons/web_chat/__init__.py create mode 100644 addons/web_chat/__openerp__.py create mode 100644 addons/web_chat/chat.py create mode 100644 addons/web_chat/static/src/css/chat.css create mode 100644 addons/web_chat/static/src/js/chat.js create mode 100644 addons/web_chat/static/src/xml/chat.xml diff --git a/addons/web_chat/__init__.py b/addons/web_chat/__init__.py new file mode 100644 index 00000000000..e61dd23b4cf --- /dev/null +++ b/addons/web_chat/__init__.py @@ -0,0 +1,2 @@ + +import chat diff --git a/addons/web_chat/__openerp__.py b/addons/web_chat/__openerp__.py new file mode 100644 index 00000000000..dd3f3cd0a07 --- /dev/null +++ b/addons/web_chat/__openerp__.py @@ -0,0 +1,19 @@ +{ + 'name' : 'Chat', + 'version': '1.0', + 'category': 'Tools', + 'complexity': 'easy', + 'description': + """ +OpenERP Chat module +=================== +Allows users to chat with each other. + """, + 'data': [], + 'depends' : [], + 'js': ['static/src/js/*.js'], + 'css': ['static/src/css/*.css'], + 'qweb': ['static/src/xml/*.xml'], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/web_chat/chat.py b/addons/web_chat/chat.py new file mode 100644 index 00000000000..3b17ea0db3e --- /dev/null +++ b/addons/web_chat/chat.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import openerp +import openerp.modules.registry +from osv import osv, fields +import gevent +import gevent.event + +class Watcher: + watchers = {} + + @staticmethod + def get_watcher(db_name): + if not Watcher.watchers.get(db_name): + Watcher(db_name) + return Watcher.watchers[db_name] + + def __init__(self, db_name): + self.db_name = db_name + Watcher.watchers[db_name] = self + self.posted = gevent.event.Event() + self.waiting = 0 + gevent.spawn(self.loop) + + def loop(self): + try: + while True: + if self.waiting == 0: + return + registry = openerp.modules.registry.RegistryManager.get(self.db_name) + with registry.cursor() as c: + conn = c._cnx + try: + c.execute("listen received_message;") + c.commit(); + if select.select([conn], [], [], 60) == ([],[],[]): + pass + else: + conn.poll() + while conn.notifies: + notify = conn.notifies.pop() + print "Got NOTIFY:", notify.pid, notify.channel, notify.payload + self.posted.set() + self.posted.clear() + finally: + try: + c.execute("unlisten received_message;") + c.commit() + except: + pass # can't do anything if that fails + finally: + del Watcher.watchers[self.db_name] + self.posted.set() + self.posted = None + + def stop(self, timeout=None): + self.waiting += 1 + self.posted.wait(timeout) + self.waiting -= 1 + + + +class chat_message(osv.osv): + _name = 'chat.message' + _columns = { + 'message': fields.char(string="Message", size=200), + } + + def poll(self, cr, uid, last=None, context=None): + num = 0 + while True: + if not last: + tmp = self.search(cr, uid, [], context=context) + last = 0 + for i in tmp: + last = i if i > last else last + res = self.search(cr, uid, [['id', '>', last]], order="id", context=context) + res = self.read(cr, uid, res, ["id", "message"], context=context) + lst = [x["message"] for x in res] + if len(lst) > 0: + plast = last + last = res[-1]["id"] + if plast is not None: + return {"res": lst, "last": last} + num += 1 + if num == 2: + return {"res": [], "last": last} + import pudb + pudb.set_trace() + Watcher.get_watcher(cr.name).stop(30) + print "waking up" + + def post(self, cr, uid, message, context=None): + self.create({"message": message}, context=context) + cr.commit() + session.execute("notify received_message, '"+ message + "'") + cr.commit() + return False diff --git a/addons/web_chat/static/src/css/chat.css b/addons/web_chat/static/src/css/chat.css new file mode 100644 index 00000000000..299cad4d5cc --- /dev/null +++ b/addons/web_chat/static/src/css/chat.css @@ -0,0 +1,6 @@ + +.openerp .oe_chat { + position: fixed; + bottom: 0px; + right: 0px; +} diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_chat/static/src/js/chat.js new file mode 100644 index 00000000000..2d19f254385 --- /dev/null +++ b/addons/web_chat/static/src/js/chat.js @@ -0,0 +1,46 @@ + +openerp.web_chat = function(instance) { + + instance.web.Menu = instance.web.Menu.extend({ + start: function() { + new instance.web_chat.Chat(instance.client).appendTo(instance.client.$el); + return this._super(); + } + }); + + instance.web_chat.Chat = instance.web.Widget.extend({ + template: "Chat", + start: function() { + var self = this; + self.poll(); + self.last = null; + self.$(".oe_chat_input").keypress(function(e) { + if(e.which != 13) { + return; + } + var mes = self.$(".oe_chat_input").val(); + self.$(".oe_chat_input").val(""); + var model = new instance.web.Model("chat.message"); + model.call("post", [mes], {context: new instance.web.CompoundContext()}).then(function() { + console.log("pushed message"); + }); + }).focus(); + }, + poll: function() { + var self = this; + var model = new instance.web.Model("chat.message"); + model.call("poll", [this.last], {context: new instance.web.CompoundContext()}).then(function(result) { + console.log("got it", result); + self.last = result.last; + _.each(result.res, function(mes) { + $("
").text(mes).appendTo(self.$(".oe_chat_content")); + }); + //self.poll(); + }, function(unused, e) { + e.preventDefault(); + //setTimeout(_.bind(self.poll, self), 5000); + }); + } + }); + +} \ No newline at end of file diff --git a/addons/web_chat/static/src/xml/chat.xml b/addons/web_chat/static/src/xml/chat.xml new file mode 100644 index 00000000000..8d364a1e48b --- /dev/null +++ b/addons/web_chat/static/src/xml/chat.xml @@ -0,0 +1,11 @@ + + + + +
+
+ +
+
+
\ No newline at end of file From 29e7b63278f2c06bd8de20027bc9e84b0a3f6222 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 16 Nov 2012 10:43:10 +0100 Subject: [PATCH 002/157] Made the chat work bzr revid: nicolas.vanhoren@openerp.com-20121116094310-jpzo8a5ij1otza47 --- addons/web_chat/chat.py | 17 +++++++---------- addons/web_chat/static/src/js/chat.js | 11 ++++------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/addons/web_chat/chat.py b/addons/web_chat/chat.py index 3b17ea0db3e..e3e211c214f 100644 --- a/addons/web_chat/chat.py +++ b/addons/web_chat/chat.py @@ -24,6 +24,7 @@ import openerp.modules.registry from osv import osv, fields import gevent import gevent.event +import select class Watcher: watchers = {} @@ -58,7 +59,6 @@ class Watcher: conn.poll() while conn.notifies: notify = conn.notifies.pop() - print "Got NOTIFY:", notify.pid, notify.channel, notify.payload self.posted.set() self.posted.clear() finally: @@ -97,21 +97,18 @@ class chat_message(osv.osv): res = self.read(cr, uid, res, ["id", "message"], context=context) lst = [x["message"] for x in res] if len(lst) > 0: - plast = last last = res[-1]["id"] - if plast is not None: - return {"res": lst, "last": last} + return {"res": lst, "last": last} num += 1 if num == 2: return {"res": [], "last": last} - import pudb - pudb.set_trace() - Watcher.get_watcher(cr.name).stop(30) - print "waking up" + cr.commit() + Watcher.get_watcher(cr.dbname).stop(30) + cr.commit() def post(self, cr, uid, message, context=None): - self.create({"message": message}, context=context) + self.create(cr, uid, {"message": message}, context=context) cr.commit() - session.execute("notify received_message, '"+ message + "'") + cr.execute("notify received_message") cr.commit() return False diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_chat/static/src/js/chat.js index 2d19f254385..6ac13ca934d 100644 --- a/addons/web_chat/static/src/js/chat.js +++ b/addons/web_chat/static/src/js/chat.js @@ -21,24 +21,21 @@ openerp.web_chat = function(instance) { var mes = self.$(".oe_chat_input").val(); self.$(".oe_chat_input").val(""); var model = new instance.web.Model("chat.message"); - model.call("post", [mes], {context: new instance.web.CompoundContext()}).then(function() { - console.log("pushed message"); - }); + model.call("post", [mes], {context: new instance.web.CompoundContext()}); }).focus(); }, poll: function() { var self = this; var model = new instance.web.Model("chat.message"); - model.call("poll", [this.last], {context: new instance.web.CompoundContext()}).then(function(result) { - console.log("got it", result); + model.call("poll", [this.last], {context: new instance.web.CompoundContext()}, {shadow: true}).then(function(result) { self.last = result.last; _.each(result.res, function(mes) { $("
").text(mes).appendTo(self.$(".oe_chat_content")); }); - //self.poll(); + self.poll(); }, function(unused, e) { e.preventDefault(); - //setTimeout(_.bind(self.poll, self), 5000); + setTimeout(_.bind(self.poll, self), 5000); }); } }); From 5e652fc45446f27b68ebb894af1c723d49464ba9 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 22 Nov 2012 15:57:03 +0100 Subject: [PATCH 003/157] [IMP] support configuration option to enable gevent bzr revid: nicolas.vanhoren@openerp.com-20121122145703-21u228hgpfsm2wtr --- addons/web_chat/chat.py | 104 +++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/addons/web_chat/chat.py b/addons/web_chat/chat.py index e3e211c214f..2edaaf2a1b7 100644 --- a/addons/web_chat/chat.py +++ b/addons/web_chat/chat.py @@ -20,62 +20,68 @@ ############################################################################## import openerp +import openerp.tools.config import openerp.modules.registry from osv import osv, fields -import gevent -import gevent.event -import select -class Watcher: - watchers = {} - @staticmethod - def get_watcher(db_name): - if not Watcher.watchers.get(db_name): - Watcher(db_name) - return Watcher.watchers[db_name] +if openerp.tools.config.options["gevent"]: + import gevent + import gevent.event + import select - def __init__(self, db_name): - self.db_name = db_name - Watcher.watchers[db_name] = self - self.posted = gevent.event.Event() - self.waiting = 0 - gevent.spawn(self.loop) + global Watcher - def loop(self): - try: - while True: - if self.waiting == 0: - return - registry = openerp.modules.registry.RegistryManager.get(self.db_name) - with registry.cursor() as c: - conn = c._cnx - try: - c.execute("listen received_message;") - c.commit(); - if select.select([conn], [], [], 60) == ([],[],[]): - pass - else: - conn.poll() - while conn.notifies: - notify = conn.notifies.pop() - self.posted.set() - self.posted.clear() - finally: + class Watcher: + watchers = {} + + @staticmethod + def get_watcher(db_name): + if not Watcher.watchers.get(db_name): + Watcher(db_name) + return Watcher.watchers[db_name] + + def __init__(self, db_name): + self.db_name = db_name + Watcher.watchers[db_name] = self + self.posted = gevent.event.Event() + self.waiting = 0 + gevent.spawn(self.loop) + + def loop(self): + try: + while True: + if self.waiting == 0: + return + registry = openerp.modules.registry.RegistryManager.get(self.db_name) + with registry.cursor() as c: + conn = c._cnx try: - c.execute("unlisten received_message;") - c.commit() - except: - pass # can't do anything if that fails - finally: - del Watcher.watchers[self.db_name] - self.posted.set() - self.posted = None + c.execute("listen received_message;") + c.commit(); + if select.select([conn], [], [], 60) == ([],[],[]): + pass + else: + conn.poll() + while conn.notifies: + notify = conn.notifies.pop() + self.posted.set() + self.posted.clear() + finally: + try: + c.execute("unlisten received_message;") + c.commit() + except: + pass # can't do anything if that fails + finally: + del Watcher.watchers[self.db_name] + self.posted.set() + self.posted = None - def stop(self, timeout=None): - self.waiting += 1 - self.posted.wait(timeout) - self.waiting -= 1 + def stop(self, timeout=None): + self.waiting += 1 + self.posted.wait(timeout) + self.waiting -= 1 @@ -86,6 +92,8 @@ class chat_message(osv.osv): } def poll(self, cr, uid, last=None, context=None): + if not openerp.tools.config.options["gevent"]: + raise Exception("Not usable in a server not running gevent") num = 0 while True: if not last: From 0e21e2006819be769461607149710c27f09b29cd Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 22 Nov 2012 16:23:02 +0100 Subject: [PATCH 004/157] Added controller for chat bzr revid: nicolas.vanhoren@openerp.com-20121122152302-qksabp48d83u0slb --- addons/web_chat/__init__.py | 1 + addons/web_chat/chat_controller.py | 12 ++++++++++++ addons/web_chat/static/src/js/chat.js | 6 ++++-- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 addons/web_chat/chat_controller.py diff --git a/addons/web_chat/__init__.py b/addons/web_chat/__init__.py index e61dd23b4cf..24f94d24ec0 100644 --- a/addons/web_chat/__init__.py +++ b/addons/web_chat/__init__.py @@ -1,2 +1,3 @@ import chat +import chat_controller diff --git a/addons/web_chat/chat_controller.py b/addons/web_chat/chat_controller.py new file mode 100644 index 00000000000..349f5e309d7 --- /dev/null +++ b/addons/web_chat/chat_controller.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +import openerp + +class ImportController(openerp.addons.web.http.Controller): + _cp_path = '/chat' + + @openerp.addons.web.http.jsonrequest + def poll(self, req, last=None): + res = req.session.model('chat.message').poll(last, req.session.eval_context(req.context)) + + return res diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_chat/static/src/js/chat.js index 6ac13ca934d..204b3f2e777 100644 --- a/addons/web_chat/static/src/js/chat.js +++ b/addons/web_chat/static/src/js/chat.js @@ -26,8 +26,10 @@ openerp.web_chat = function(instance) { }, poll: function() { var self = this; - var model = new instance.web.Model("chat.message"); - model.call("poll", [this.last], {context: new instance.web.CompoundContext()}, {shadow: true}).then(function(result) { + this.rpc("/chat/poll", { + last: this.last, + context: new instance.web.CompoundContext() + }, {shadow: true}).then(function(result) { self.last = result.last; _.each(result.res, function(mes) { $("
").text(mes).appendTo(self.$(".oe_chat_content")); From 2355be5fde58d34389a126ad3586c5ec9a02a759 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 22 Nov 2012 16:31:28 +0100 Subject: [PATCH 005/157] Extracted the polling method to be outside of a model bzr revid: nicolas.vanhoren@openerp.com-20121122153128-8lkqq7het8nhpq94 --- addons/web_chat/chat.py | 34 +++++++++++------------------- addons/web_chat/chat_controller.py | 16 +++++++++++--- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/addons/web_chat/chat.py b/addons/web_chat/chat.py index 2edaaf2a1b7..8290fb64bc0 100644 --- a/addons/web_chat/chat.py +++ b/addons/web_chat/chat.py @@ -91,28 +91,18 @@ class chat_message(osv.osv): 'message': fields.char(string="Message", size=200), } - def poll(self, cr, uid, last=None, context=None): - if not openerp.tools.config.options["gevent"]: - raise Exception("Not usable in a server not running gevent") - num = 0 - while True: - if not last: - tmp = self.search(cr, uid, [], context=context) - last = 0 - for i in tmp: - last = i if i > last else last - res = self.search(cr, uid, [['id', '>', last]], order="id", context=context) - res = self.read(cr, uid, res, ["id", "message"], context=context) - lst = [x["message"] for x in res] - if len(lst) > 0: - last = res[-1]["id"] - return {"res": lst, "last": last} - num += 1 - if num == 2: - return {"res": [], "last": last} - cr.commit() - Watcher.get_watcher(cr.dbname).stop(30) - cr.commit() + def get_messages(self, cr, uid, last=None, context=None): + if not last: + tmp = self.search(cr, uid, [], context=context) + last = 0 + for i in tmp: + last = i if i > last else last + res = self.search(cr, uid, [['id', '>', last]], order="id", context=context) + res = self.read(cr, uid, res, ["id", "message"], context=context) + lst = [x["message"] for x in res] + if len(lst) > 0: + last = res[-1]["id"] + return {"res": lst, "last": last, "dbname": cr.dbname} def post(self, cr, uid, message, context=None): self.create(cr, uid, {"message": message}, context=context) diff --git a/addons/web_chat/chat_controller.py b/addons/web_chat/chat_controller.py index 349f5e309d7..8f22c460fa1 100644 --- a/addons/web_chat/chat_controller.py +++ b/addons/web_chat/chat_controller.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- import openerp +import openerp.tools.config +import chat class ImportController(openerp.addons.web.http.Controller): _cp_path = '/chat' @openerp.addons.web.http.jsonrequest def poll(self, req, last=None): - res = req.session.model('chat.message').poll(last, req.session.eval_context(req.context)) - - return res + if not openerp.tools.config.options["gevent"]: + raise Exception("Not usable in a server not running gevent") + num = 0 + while True: + res = req.session.model('chat.message').get_messages(last, req.session.eval_context(req.context)) + if num >= 1 or len(res["res"]) > 0: + return res + last = res["last"] + num += 1 + print "waiting" + chat.Watcher.get_watcher(res["dbname"]).stop(30) From cfd970a982a75efb92a8c86e12d78d3340759a4e Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 26 Nov 2012 13:38:53 +0100 Subject: [PATCH 006/157] Added chat panel bzr revid: nicolas.vanhoren@openerp.com-20121126123853-9xetblf78kr0qy3c --- addons/web_chat/static/src/css/chat.css | 9 ++++- addons/web_chat/static/src/js/chat.js | 54 ++++++++++++++++++++++--- addons/web_chat/static/src/xml/chat.xml | 5 +++ 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/addons/web_chat/static/src/css/chat.css b/addons/web_chat/static/src/css/chat.css index 299cad4d5cc..887d8b2fef2 100644 --- a/addons/web_chat/static/src/css/chat.css +++ b/addons/web_chat/static/src/css/chat.css @@ -1,6 +1,11 @@ .openerp .oe_chat { position: fixed; - bottom: 0px; - right: 0px; + background-color: #E8EBEF; + width: 220px; + border-left: 1px solid #AEB9BD; } + +.openerp .oe_topbar_chatbutton{ + +} \ No newline at end of file diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_chat/static/src/js/chat.js index 204b3f2e777..51259ae417d 100644 --- a/addons/web_chat/static/src/js/chat.js +++ b/addons/web_chat/static/src/js/chat.js @@ -1,19 +1,44 @@ openerp.web_chat = function(instance) { - instance.web.Menu = instance.web.Menu.extend({ - start: function() { - new instance.web_chat.Chat(instance.client).appendTo(instance.client.$el); - return this._super(); - } + instance.web.UserMenu.include({ + do_update: function(){ + var self = this; + this.update_promise.then(function() { + var chat = new instance.web_chat.Chat(self); + chat.appendTo(instance.client.$el); + var button = new instance.web.ChatTopButton(self); + button.chat = chat; + button.appendTo(instance.webclient.$el.find('.oe_systray')); + }); + return this._super.apply(this, arguments); + }, + }); + + instance.web.ChatTopButton = instance.web.Widget.extend({ + template:'ChatTopButton', + events: { + "click button": "clicked", + }, + clicked: function() { + this.chat.switch_display(); + }, }); instance.web_chat.Chat = instance.web.Widget.extend({ template: "Chat", + init: function(parent) { + this._super(parent); + this.shown = false; + }, start: function() { var self = this; + this.$el.css("right", -this.$el.outerWidth()); self.poll(); self.last = null; + $(window).scroll(_.bind(this.calc_box, this)); + $(window).resize(_.bind(this.calc_box, this)); + self.calc_box(); self.$(".oe_chat_input").keypress(function(e) { if(e.which != 13) { return; @@ -24,6 +49,25 @@ openerp.web_chat = function(instance) { model.call("post", [mes], {context: new instance.web.CompoundContext()}); }).focus(); }, + calc_box: function() { + var $topbar = instance.client.$(".oe_topbar"); + var top = $topbar.offset().top + $topbar.height(); + top = Math.max(top - $(window).scrollTop(), 0); + this.$el.css("top", top); + this.$el.css("bottom", 0); + }, + switch_display: function() { + if (this.shown) { + this.$el.animate({ + right: -this.$el.outerWidth(), + }); + } else { + this.$el.animate({ + right: 0, + }); + } + this.shown = ! this.shown; + }, poll: function() { var self = this; this.rpc("/chat/poll", { diff --git a/addons/web_chat/static/src/xml/chat.xml b/addons/web_chat/static/src/xml/chat.xml index 8d364a1e48b..c6f0499b2b1 100644 --- a/addons/web_chat/static/src/xml/chat.xml +++ b/addons/web_chat/static/src/xml/chat.xml @@ -8,4 +8,9 @@
+ +
+ +
+
\ No newline at end of file From 38dc28f46b688f175b594a196f86d041b29ab4ab Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 26 Nov 2012 13:55:48 +0100 Subject: [PATCH 007/157] Added offset calculus bzr revid: nicolas.vanhoren@openerp.com-20121126125548-qtvib1l8102vvd64 --- addons/web_chat/static/src/js/chat.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_chat/static/src/js/chat.js index 51259ae417d..cc3b756931e 100644 --- a/addons/web_chat/static/src/js/chat.js +++ b/addons/web_chat/static/src/js/chat.js @@ -30,6 +30,7 @@ openerp.web_chat = function(instance) { init: function(parent) { this._super(parent); this.shown = false; + this.set("right_offset", 0); }, start: function() { var self = this; @@ -57,14 +58,21 @@ openerp.web_chat = function(instance) { this.$el.css("bottom", 0); }, switch_display: function() { + var fct = _.bind(function() { + this.set("right_offset", $(window).width() - this.$el.offset().left); + }, this); + var opt = { + step: fct, + complete: fct, + }; if (this.shown) { this.$el.animate({ right: -this.$el.outerWidth(), - }); + }, opt); } else { this.$el.animate({ right: 0, - }); + }, opt); } this.shown = ! this.shown; }, From 01e12e5462ce5568be7b8ee5052bdf17fc664526 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 11:02:41 +0100 Subject: [PATCH 008/157] Renammed chat to im bzr revid: nicolas.vanhoren@openerp.com-20121127100241-qq43we149rxpclk8 --- addons/web_chat/__init__.py | 3 -- addons/web_chat/static/src/xml/chat.xml | 16 ---------- addons/web_im/__init__.py | 3 ++ addons/{web_chat => web_im}/__openerp__.py | 2 +- addons/{web_chat/chat.py => web_im/im.py} | 4 +-- .../im_controller.py} | 8 ++--- .../chat.css => web_im/static/src/css/im.css} | 4 +-- .../js/chat.js => web_im/static/src/js/im.js} | 32 +++++++++---------- addons/web_im/static/src/xml/im.xml | 16 ++++++++++ 9 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 addons/web_chat/__init__.py delete mode 100644 addons/web_chat/static/src/xml/chat.xml create mode 100644 addons/web_im/__init__.py rename addons/{web_chat => web_im}/__openerp__.py (92%) rename addons/{web_chat/chat.py => web_im/im.py} (98%) rename addons/{web_chat/chat_controller.py => web_im/im_controller.py} (71%) rename addons/{web_chat/static/src/css/chat.css => web_im/static/src/css/im.css} (68%) rename addons/{web_chat/static/src/js/chat.js => web_im/static/src/js/im.js} (75%) create mode 100644 addons/web_im/static/src/xml/im.xml diff --git a/addons/web_chat/__init__.py b/addons/web_chat/__init__.py deleted file mode 100644 index 24f94d24ec0..00000000000 --- a/addons/web_chat/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -import chat -import chat_controller diff --git a/addons/web_chat/static/src/xml/chat.xml b/addons/web_chat/static/src/xml/chat.xml deleted file mode 100644 index c6f0499b2b1..00000000000 --- a/addons/web_chat/static/src/xml/chat.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - -
-
- -
-
- -
- -
-
-
\ No newline at end of file diff --git a/addons/web_im/__init__.py b/addons/web_im/__init__.py new file mode 100644 index 00000000000..399ad665c23 --- /dev/null +++ b/addons/web_im/__init__.py @@ -0,0 +1,3 @@ + +import im +import im_controller diff --git a/addons/web_chat/__openerp__.py b/addons/web_im/__openerp__.py similarity index 92% rename from addons/web_chat/__openerp__.py rename to addons/web_im/__openerp__.py index dd3f3cd0a07..438c33c3e2c 100644 --- a/addons/web_chat/__openerp__.py +++ b/addons/web_im/__openerp__.py @@ -1,5 +1,5 @@ { - 'name' : 'Chat', + 'name' : 'Instant Messaging', 'version': '1.0', 'category': 'Tools', 'complexity': 'easy', diff --git a/addons/web_chat/chat.py b/addons/web_im/im.py similarity index 98% rename from addons/web_chat/chat.py rename to addons/web_im/im.py index 8290fb64bc0..2bf83dbc7d4 100644 --- a/addons/web_chat/chat.py +++ b/addons/web_im/im.py @@ -85,8 +85,8 @@ if openerp.tools.config.options["gevent"]: -class chat_message(osv.osv): - _name = 'chat.message' +class im_message(osv.osv): + _name = 'im.message' _columns = { 'message': fields.char(string="Message", size=200), } diff --git a/addons/web_chat/chat_controller.py b/addons/web_im/im_controller.py similarity index 71% rename from addons/web_chat/chat_controller.py rename to addons/web_im/im_controller.py index 8f22c460fa1..8cd5fe4505e 100644 --- a/addons/web_chat/chat_controller.py +++ b/addons/web_im/im_controller.py @@ -2,10 +2,10 @@ import openerp import openerp.tools.config -import chat +import im class ImportController(openerp.addons.web.http.Controller): - _cp_path = '/chat' + _cp_path = '/im' @openerp.addons.web.http.jsonrequest def poll(self, req, last=None): @@ -13,10 +13,10 @@ class ImportController(openerp.addons.web.http.Controller): raise Exception("Not usable in a server not running gevent") num = 0 while True: - res = req.session.model('chat.message').get_messages(last, req.session.eval_context(req.context)) + res = req.session.model('im.message').get_messages(last, req.session.eval_context(req.context)) if num >= 1 or len(res["res"]) > 0: return res last = res["last"] num += 1 print "waiting" - chat.Watcher.get_watcher(res["dbname"]).stop(30) + im.Watcher.get_watcher(res["dbname"]).stop(30) diff --git a/addons/web_chat/static/src/css/chat.css b/addons/web_im/static/src/css/im.css similarity index 68% rename from addons/web_chat/static/src/css/chat.css rename to addons/web_im/static/src/css/im.css index 887d8b2fef2..e804679cbfc 100644 --- a/addons/web_chat/static/src/css/chat.css +++ b/addons/web_im/static/src/css/im.css @@ -1,11 +1,11 @@ -.openerp .oe_chat { +.openerp .oe_im { position: fixed; background-color: #E8EBEF; width: 220px; border-left: 1px solid #AEB9BD; } -.openerp .oe_topbar_chatbutton{ +.openerp .oe_topbar_imbutton{ } \ No newline at end of file diff --git a/addons/web_chat/static/src/js/chat.js b/addons/web_im/static/src/js/im.js similarity index 75% rename from addons/web_chat/static/src/js/chat.js rename to addons/web_im/static/src/js/im.js index cc3b756931e..dd88d0a611e 100644 --- a/addons/web_chat/static/src/js/chat.js +++ b/addons/web_im/static/src/js/im.js @@ -1,32 +1,32 @@ -openerp.web_chat = function(instance) { +openerp.web_im = function(instance) { instance.web.UserMenu.include({ do_update: function(){ var self = this; this.update_promise.then(function() { - var chat = new instance.web_chat.Chat(self); - chat.appendTo(instance.client.$el); - var button = new instance.web.ChatTopButton(self); - button.chat = chat; + var im = new instance.web_im.InstantMessaging(self); + im.appendTo(instance.client.$el); + var button = new instance.web.IMTopButton(self); + button.im = im; button.appendTo(instance.webclient.$el.find('.oe_systray')); }); return this._super.apply(this, arguments); }, }); - instance.web.ChatTopButton = instance.web.Widget.extend({ - template:'ChatTopButton', + instance.web.IMTopButton = instance.web.Widget.extend({ + template:'IMTopButton', events: { "click button": "clicked", }, clicked: function() { - this.chat.switch_display(); + this.im.switch_display(); }, }); - instance.web_chat.Chat = instance.web.Widget.extend({ - template: "Chat", + instance.web_im.InstantMessaging = instance.web.Widget.extend({ + template: "InstantMessaging", init: function(parent) { this._super(parent); this.shown = false; @@ -40,13 +40,13 @@ openerp.web_chat = function(instance) { $(window).scroll(_.bind(this.calc_box, this)); $(window).resize(_.bind(this.calc_box, this)); self.calc_box(); - self.$(".oe_chat_input").keypress(function(e) { + self.$(".oe_im_input").keypress(function(e) { if(e.which != 13) { return; } - var mes = self.$(".oe_chat_input").val(); - self.$(".oe_chat_input").val(""); - var model = new instance.web.Model("chat.message"); + var mes = self.$(".oe_im_input").val(); + self.$(".oe_im_input").val(""); + var model = new instance.web.Model("im.message"); model.call("post", [mes], {context: new instance.web.CompoundContext()}); }).focus(); }, @@ -78,13 +78,13 @@ openerp.web_chat = function(instance) { }, poll: function() { var self = this; - this.rpc("/chat/poll", { + this.rpc("/im/poll", { last: this.last, context: new instance.web.CompoundContext() }, {shadow: true}).then(function(result) { self.last = result.last; _.each(result.res, function(mes) { - $("
").text(mes).appendTo(self.$(".oe_chat_content")); + $("
").text(mes).appendTo(self.$(".oe_im_content")); }); self.poll(); }, function(unused, e) { diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml new file mode 100644 index 00000000000..f87a0c1c531 --- /dev/null +++ b/addons/web_im/static/src/xml/im.xml @@ -0,0 +1,16 @@ + + + + +
+
+ +
+
+ +
+ +
+
+
\ No newline at end of file From a77339ccf0b0925f217508a65bdc1cfffe5abd5c Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 11:06:15 +0100 Subject: [PATCH 009/157] Fusion python files bzr revid: nicolas.vanhoren@openerp.com-20121127100615-07mv10gxj19y43v4 --- addons/web_im/__init__.py | 1 - addons/web_im/im.py | 17 +++++++++++++++++ addons/web_im/im_controller.py | 22 ---------------------- 3 files changed, 17 insertions(+), 23 deletions(-) delete mode 100644 addons/web_im/im_controller.py diff --git a/addons/web_im/__init__.py b/addons/web_im/__init__.py index 399ad665c23..23c6ad13350 100644 --- a/addons/web_im/__init__.py +++ b/addons/web_im/__init__.py @@ -1,3 +1,2 @@ import im -import im_controller diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 2bf83dbc7d4..b2ff83d6f7d 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -84,6 +84,23 @@ if openerp.tools.config.options["gevent"]: self.waiting -= 1 +class ImportController(openerp.addons.web.http.Controller): + _cp_path = '/im' + + @openerp.addons.web.http.jsonrequest + def poll(self, req, last=None): + if not openerp.tools.config.options["gevent"]: + raise Exception("Not usable in a server not running gevent") + num = 0 + while True: + res = req.session.model('im.message').get_messages(last, req.session.eval_context(req.context)) + if num >= 1 or len(res["res"]) > 0: + return res + last = res["last"] + num += 1 + print "waiting" + Watcher.get_watcher(res["dbname"]).stop(30) + class im_message(osv.osv): _name = 'im.message' diff --git a/addons/web_im/im_controller.py b/addons/web_im/im_controller.py deleted file mode 100644 index 8cd5fe4505e..00000000000 --- a/addons/web_im/im_controller.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- - -import openerp -import openerp.tools.config -import im - -class ImportController(openerp.addons.web.http.Controller): - _cp_path = '/im' - - @openerp.addons.web.http.jsonrequest - def poll(self, req, last=None): - if not openerp.tools.config.options["gevent"]: - raise Exception("Not usable in a server not running gevent") - num = 0 - while True: - res = req.session.model('im.message').get_messages(last, req.session.eval_context(req.context)) - if num >= 1 or len(res["res"]) > 0: - return res - last = res["last"] - num += 1 - print "waiting" - im.Watcher.get_watcher(res["dbname"]).stop(30) From 986f9d26f3edee586d4a7536a3d0c24d13c71ca4 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 11:30:52 +0100 Subject: [PATCH 010/157] Implemented user search bzr revid: nicolas.vanhoren@openerp.com-20121127103052-nb09c5x0e1t485za --- addons/web_im/static/src/js/im.js | 61 +++++++++++++++++++++-------- addons/web_im/static/src/xml/im.xml | 10 ++++- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index dd88d0a611e..cdeb7fc16e0 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -7,7 +7,7 @@ openerp.web_im = function(instance) { this.update_promise.then(function() { var im = new instance.web_im.InstantMessaging(self); im.appendTo(instance.client.$el); - var button = new instance.web.IMTopButton(self); + var button = new instance.web.ImTopButton(self); button.im = im; button.appendTo(instance.webclient.$el.find('.oe_systray')); }); @@ -15,8 +15,8 @@ openerp.web_im = function(instance) { }, }); - instance.web.IMTopButton = instance.web.Widget.extend({ - template:'IMTopButton', + instance.web.ImTopButton = instance.web.Widget.extend({ + template:'ImTopButton', events: { "click button": "clicked", }, @@ -27,28 +27,24 @@ openerp.web_im = function(instance) { instance.web_im.InstantMessaging = instance.web.Widget.extend({ template: "InstantMessaging", + events: { + "keydown .oe_im_input": "search_users", + }, init: function(parent) { this._super(parent); this.shown = false; this.set("right_offset", 0); + this.last = null; + this.users = []; }, start: function() { - var self = this; this.$el.css("right", -this.$el.outerWidth()); - self.poll(); - self.last = null; $(window).scroll(_.bind(this.calc_box, this)); $(window).resize(_.bind(this.calc_box, this)); - self.calc_box(); - self.$(".oe_im_input").keypress(function(e) { - if(e.which != 13) { - return; - } - var mes = self.$(".oe_im_input").val(); - self.$(".oe_im_input").val(""); - var model = new instance.web.Model("im.message"); - model.call("post", [mes], {context: new instance.web.CompoundContext()}); - }).focus(); + this.calc_box(); + + this.poll(); + this.search_users(); }, calc_box: function() { var $topbar = instance.client.$(".oe_topbar"); @@ -57,6 +53,31 @@ openerp.web_im = function(instance) { this.$el.css("top", top); this.$el.css("bottom", 0); }, + search_users: function(e) { + if(e && e.which !== 13) { + return; + } + var users = new instance.web.Model("res.users"); + var self = this; + return users.query(["name"]).filter([["name", "ilike", this.$(".oe_im_input").val()]]).limit(20).all().then(function(result) { + _.each(self.users, function(user) { + user.destroy(); + }); + self.users = []; + _.each(result, function(user) { + var widget = new instance.web_im.ImUser(self, user); + widget.appendTo(self.$(".oe_im_users")); + self.users.push(widget); + }); + }); + }, + send_message: function() { + // old code + var mes = self.$(".oe_im_input").val(); + self.$(".oe_im_input").val(""); + var model = new instance.web.Model("im.message"); + model.call("post", [mes], {context: new instance.web.CompoundContext()}); + }, switch_display: function() { var fct = _.bind(function() { this.set("right_offset", $(window).width() - this.$el.offset().left); @@ -94,4 +115,12 @@ openerp.web_im = function(instance) { } }); + instance.web_im.ImUser = instance.web.Widget.extend({ + "template": "ImUser", + init: function(parent, user_rec) { + this._super(parent); + this.user_rec = user_rec; + }, + }); + } \ No newline at end of file diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index f87a0c1c531..c6b4f01b8e7 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -4,13 +4,19 @@
-
+
+
- +
+ +
+ +
+
\ No newline at end of file From 97de13cea0415676d8330417b277de0902cd03ca Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 11:56:35 +0100 Subject: [PATCH 011/157] Partial implementation of chat from user to user bzr revid: nicolas.vanhoren@openerp.com-20121127105635-qrca5nt9eq4i9o7d --- addons/web_im/static/src/css/im.css | 9 +++++ addons/web_im/static/src/js/im.js | 53 ++++++++++++++++++++++++----- addons/web_im/static/src/xml/im.xml | 8 ++++- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index e804679cbfc..96180fcba9c 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -8,4 +8,13 @@ .openerp .oe_topbar_imbutton{ +} + +.openerp .oe_im_conversation { + position: fixed; + bottom: 0px; + background-color: #E8EBEF; + margin: 10px; + width: 180px; + border: 1px solid #AEB9BD; } \ No newline at end of file diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index cdeb7fc16e0..28386a67f7b 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -7,8 +7,8 @@ openerp.web_im = function(instance) { this.update_promise.then(function() { var im = new instance.web_im.InstantMessaging(self); im.appendTo(instance.client.$el); - var button = new instance.web.ImTopButton(self); - button.im = im; + var button = new instance.web.ImTopButton(this); + button.on("clicked", im, im.switch_display); button.appendTo(instance.webclient.$el.find('.oe_systray')); }); return this._super.apply(this, arguments); @@ -21,7 +21,7 @@ openerp.web_im = function(instance) { "click button": "clicked", }, clicked: function() { - this.im.switch_display(); + this.trigger("clicked"); }, }); @@ -60,6 +60,7 @@ openerp.web_im = function(instance) { var users = new instance.web.Model("res.users"); var self = this; return users.query(["name"]).filter([["name", "ilike", this.$(".oe_im_input").val()]]).limit(20).all().then(function(result) { + self.$(".oe_im_input").val(""); _.each(self.users, function(user) { user.destroy(); }); @@ -67,14 +68,12 @@ openerp.web_im = function(instance) { _.each(result, function(user) { var widget = new instance.web_im.ImUser(self, user); widget.appendTo(self.$(".oe_im_users")); + widget.on("activate_user", self, self.activate_user); self.users.push(widget); }); }); }, - send_message: function() { - // old code - var mes = self.$(".oe_im_input").val(); - self.$(".oe_im_input").val(""); + send_message: function(user_rec, mes) { var model = new instance.web.Model("im.message"); model.call("post", [mes], {context: new instance.web.CompoundContext()}); }, @@ -112,15 +111,53 @@ openerp.web_im = function(instance) { e.preventDefault(); setTimeout(_.bind(self.poll, self), 5000); }); - } + }, + activate_user: function(user_rec) { + // shitty, to replace + var conv = new instance.web_im.Conversation(this, this, user_rec); + conv.appendTo(this.$el); + }, }); instance.web_im.ImUser = instance.web.Widget.extend({ "template": "ImUser", + events: { + "click": "activate_user", + }, init: function(parent, user_rec) { this._super(parent); this.user_rec = user_rec; }, + activate_user: function() { + this.trigger("activate_user", this.user_rec); + }, + }); + + instance.web_im.Conversation = instance.web.Widget.extend({ + "template": "Conversation", + events: { + "keydown input": "send_message", + }, + init: function(parent, im, user_rec) { + this._super(parent); + this.im = im; + this.user_rec = user_rec; + }, + start: function() { + this.im.on("change:right_offset", this, this.calc_pos); + this.calc_pos(); + }, + calc_pos: function() { + this.$el.css("right", this.im.get("right_offset")); + }, + send_message: function(e) { + if(e && e.which !== 13) { + return; + } + var mes = this.$("input").val(); + this.$("input").val(""); + this.im.send_message(this.user_rec, mes); + }, }); } \ No newline at end of file diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index c6b4f01b8e7..c54e9b85869 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -5,8 +5,8 @@
-
+
@@ -19,4 +19,10 @@
+ +
+
+ +
+
\ No newline at end of file From 80f2419cfe6125a868f0abfd3ea7a7f372fb2ecc Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 12:07:18 +0100 Subject: [PATCH 012/157] Optimized first search bzr revid: nicolas.vanhoren@openerp.com-20121127110718-mxscp8uafwfuf7bl --- addons/web_im/im.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index b2ff83d6f7d..11d02ade45f 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -105,15 +105,13 @@ class ImportController(openerp.addons.web.http.Controller): class im_message(osv.osv): _name = 'im.message' _columns = { - 'message': fields.char(string="Message", size=200), + 'message': fields.char(string="Message", size=200, required=True), } def get_messages(self, cr, uid, last=None, context=None): if not last: - tmp = self.search(cr, uid, [], context=context) - last = 0 - for i in tmp: - last = i if i > last else last + tmp = self.search(cr, uid, [], order="id desc", limit=1, context=context) + last = tmp[0] if len(tmp) else 0 res = self.search(cr, uid, [['id', '>', last]], order="id", context=context) res = self.read(cr, uid, res, ["id", "message"], context=context) lst = [x["message"] for x in res] From 69db26392f987fb0b82b867f02c54e09ddeb2a2f Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 14:51:55 +0100 Subject: [PATCH 013/157] Implemented message to specific user bzr revid: nicolas.vanhoren@openerp.com-20121127135155-7ndbfraqw0g7rn78 --- addons/web_im/im.py | 12 +++++++----- addons/web_im/static/src/js/im.js | 7 ++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 11d02ade45f..2ecc1d23c01 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -106,21 +106,23 @@ class im_message(osv.osv): _name = 'im.message' _columns = { 'message': fields.char(string="Message", size=200, required=True), + 'from': fields.many2one("res.users", "From", required= True, ondelete='cascade'), + 'to': fields.many2one("res.users", "From", required=True, select=True, ondelete='cascade'), } def get_messages(self, cr, uid, last=None, context=None): if not last: - tmp = self.search(cr, uid, [], order="id desc", limit=1, context=context) - last = tmp[0] if len(tmp) else 0 - res = self.search(cr, uid, [['id', '>', last]], order="id", context=context) + tmp = self.search(cr, uid, [['to', '=', uid]], order="id desc", limit=1, context=context) + last = tmp[0] if len(tmp) >= 1 else -1 + res = self.search(cr, uid, [['id', '>', last], ['to', '=', uid]], order="id", context=context) res = self.read(cr, uid, res, ["id", "message"], context=context) lst = [x["message"] for x in res] if len(lst) > 0: last = res[-1]["id"] return {"res": lst, "last": last, "dbname": cr.dbname} - def post(self, cr, uid, message, context=None): - self.create(cr, uid, {"message": message}, context=context) + def post(self, cr, uid, message, to_user_id, context=None): + self.create(cr, uid, {"message": message, 'from': uid, 'to': to_user_id}, context=context) cr.commit() cr.execute("notify received_message") cr.commit() diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 28386a67f7b..123ec4d1c0b 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -73,10 +73,6 @@ openerp.web_im = function(instance) { }); }); }, - send_message: function(user_rec, mes) { - var model = new instance.web.Model("im.message"); - model.call("post", [mes], {context: new instance.web.CompoundContext()}); - }, switch_display: function() { var fct = _.bind(function() { this.set("right_offset", $(window).width() - this.$el.offset().left); @@ -156,7 +152,8 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this.im.send_message(this.user_rec, mes); + var model = new instance.web.Model("im.message"); + model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); }, }); From f48a0c4a8bda49c229d89dffa03855438d667430 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 15:47:24 +0100 Subject: [PATCH 014/157] put global variables for fine tuning bzr revid: nicolas.vanhoren@openerp.com-20121127144724-8b9tc0ix6xplpojp --- addons/web_im/im.py | 6 ++++-- addons/web_im/static/src/js/im.js | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 2ecc1d23c01..a16203947f1 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -24,6 +24,8 @@ import openerp.tools.config import openerp.modules.registry from osv import osv, fields +WATCHER_TIMER = 60 +POLL_TIMER = 30 if openerp.tools.config.options["gevent"]: import gevent @@ -59,7 +61,7 @@ if openerp.tools.config.options["gevent"]: try: c.execute("listen received_message;") c.commit(); - if select.select([conn], [], [], 60) == ([],[],[]): + if select.select([conn], [], [], WATCHER_TIMER) == ([],[],[]): pass else: conn.poll() @@ -99,7 +101,7 @@ class ImportController(openerp.addons.web.http.Controller): last = res["last"] num += 1 print "waiting" - Watcher.get_watcher(res["dbname"]).stop(30) + Watcher.get_watcher(res["dbname"]).stop(POLL_TIMER) class im_message(osv.osv): diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 123ec4d1c0b..92eab8d54ff 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -1,6 +1,9 @@ openerp.web_im = function(instance) { + var USERS_LIMIT = 20; + var ERROR_DELAY = 5000; + instance.web.UserMenu.include({ do_update: function(){ var self = this; @@ -59,7 +62,7 @@ openerp.web_im = function(instance) { } var users = new instance.web.Model("res.users"); var self = this; - return users.query(["name"]).filter([["name", "ilike", this.$(".oe_im_input").val()]]).limit(20).all().then(function(result) { + return users.query(["name"]).filter([["name", "ilike", this.$(".oe_im_input").val()]]).limit(USERS_LIMIT).all().then(function(result) { self.$(".oe_im_input").val(""); _.each(self.users, function(user) { user.destroy(); @@ -105,7 +108,7 @@ openerp.web_im = function(instance) { self.poll(); }, function(unused, e) { e.preventDefault(); - setTimeout(_.bind(self.poll, self), 5000); + setTimeout(_.bind(self.poll, self), ERROR_DELAY); }); }, activate_user: function(user_rec) { From f4cf8a0e0a3d6ab74b7275ca64f978470ee3a3b9 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 16:29:55 +0100 Subject: [PATCH 015/157] Perfected dialog between users bzr revid: nicolas.vanhoren@openerp.com-20121127152955-jb3ym4xidjcyh1ar --- addons/web_im/im.py | 8 ++-- addons/web_im/static/src/js/im.js | 66 ++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index a16203947f1..4667827d979 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -100,7 +100,6 @@ class ImportController(openerp.addons.web.http.Controller): return res last = res["last"] num += 1 - print "waiting" Watcher.get_watcher(res["dbname"]).stop(POLL_TIMER) @@ -117,11 +116,10 @@ class im_message(osv.osv): tmp = self.search(cr, uid, [['to', '=', uid]], order="id desc", limit=1, context=context) last = tmp[0] if len(tmp) >= 1 else -1 res = self.search(cr, uid, [['id', '>', last], ['to', '=', uid]], order="id", context=context) - res = self.read(cr, uid, res, ["id", "message"], context=context) - lst = [x["message"] for x in res] - if len(lst) > 0: + res = self.read(cr, uid, res, ["id", "message", "from"], context=context) + if len(res) > 0: last = res[-1]["id"] - return {"res": lst, "last": last, "dbname": cr.dbname} + return {"res": res, "last": last, "dbname": cr.dbname} def post(self, cr, uid, message, to_user_id, context=None): self.create(cr, uid, {"message": message, 'from': uid, 'to': to_user_id}, context=context) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 92eab8d54ff..1c55f6ac491 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -10,7 +10,7 @@ openerp.web_im = function(instance) { this.update_promise.then(function() { var im = new instance.web_im.InstantMessaging(self); im.appendTo(instance.client.$el); - var button = new instance.web.ImTopButton(this); + var button = new instance.web_im.ImTopButton(this); button.on("clicked", im, im.switch_display); button.appendTo(instance.webclient.$el.find('.oe_systray')); }); @@ -18,7 +18,7 @@ openerp.web_im = function(instance) { }, }); - instance.web.ImTopButton = instance.web.Widget.extend({ + instance.web_im.ImTopButton = instance.web.Widget.extend({ template:'ImTopButton', events: { "click button": "clicked", @@ -39,6 +39,10 @@ openerp.web_im = function(instance) { this.set("right_offset", 0); this.last = null; this.users = []; + this.c_manager = new instance.web_im.ConversationManager(this); + this.on("change:right_offset", this.c_manager, _.bind(function() { + this.c_manager.set("right_offset", this.get("right_offset")); + }, this)); }, start: function() { this.$el.css("right", -this.$el.outerWidth()); @@ -103,7 +107,7 @@ openerp.web_im = function(instance) { }, {shadow: true}).then(function(result) { self.last = result.last; _.each(result.res, function(mes) { - $("
").text(mes).appendTo(self.$(".oe_im_content")); + self.c_manager.received_message(mes, {"id": mes.from[0]}); }); self.poll(); }, function(unused, e) { @@ -112,9 +116,7 @@ openerp.web_im = function(instance) { }); }, activate_user: function(user_rec) { - // shitty, to replace - var conv = new instance.web_im.Conversation(this, this, user_rec); - conv.appendTo(this.$el); + this.c_manager.activate_user(user_rec); }, }); @@ -132,22 +134,61 @@ openerp.web_im = function(instance) { }, }); + instance.web_im.ConversationManager = instance.web.Controller.extend({ + init: function(parent) { + this._super(parent); + this.set("right_offset", 0); + this.conversations = []; + this.users = {}; + this.on("change:right_offset", this, this.calc_positions); + }, + activate_user: function(user_rec) { + if (this.users[user_rec.id]) { + return this.users[user_rec.id]; + } + var conv = new instance.web_im.Conversation(this, user_rec); + conv.appendTo(instance.client.$el); + conv.on("destroyed", this, function() { + this.conversations = _.without(this.conversations, conv); + delete this.users[conv.user_rec.id]; + }); + this.conversations.push(conv); + this.users[user_rec.id] = conv; + this.calc_positions(); + return conv; + }, + received_message: function(message, user_rec) { + var conv = this.activate_user(user_rec); + conv.received_message(message); + }, + calc_positions: function() { + var current = this.get("right_offset"); + _.each(_.range(this.conversations.length), function(i) { + this.conversations[i].set("right_position", current); + current += this.conversations[i].$el.outerWidth(); + }, this); + }, + }); + instance.web_im.Conversation = instance.web.Widget.extend({ "template": "Conversation", events: { "keydown input": "send_message", }, - init: function(parent, im, user_rec) { + init: function(parent, user_rec) { this._super(parent); - this.im = im; this.user_rec = user_rec; + this.set("right_position", 0); }, start: function() { - this.im.on("change:right_offset", this, this.calc_pos); + this.on("change:right_position", this, this.calc_pos); this.calc_pos(); }, calc_pos: function() { - this.$el.css("right", this.im.get("right_offset")); + this.$el.css("right", this.get("right_position")); + }, + received_message: function(message) { + this.$(".oe_conversation_text").append($("
").text("Him: " + message.message)); }, send_message: function(e) { if(e && e.which !== 13) { @@ -155,9 +196,14 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); + this.$(".oe_conversation_text").append($("
").text("Me: " + mes)); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); }, + destroy: function() { + this.trigger("destroyed"); + return this._super(); + }, }); } \ No newline at end of file From 8822115efd60a6028d05ac55af2d00c1245e3e1d Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 16:41:32 +0100 Subject: [PATCH 016/157] Added security bzr revid: nicolas.vanhoren@openerp.com-20121127154132-vbtwc0drcg13kpwg --- addons/web_im/__openerp__.py | 4 +++- addons/web_im/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 addons/web_im/security/ir.model.access.csv diff --git a/addons/web_im/__openerp__.py b/addons/web_im/__openerp__.py index 438c33c3e2c..7353400b9e0 100644 --- a/addons/web_im/__openerp__.py +++ b/addons/web_im/__openerp__.py @@ -9,7 +9,9 @@ OpenERP Chat module =================== Allows users to chat with each other. """, - 'data': [], + 'data': [ + 'security/ir.model.access.csv', + ], 'depends' : [], 'js': ['static/src/js/*.js'], 'css': ['static/src/css/*.css'], diff --git a/addons/web_im/security/ir.model.access.csv b/addons/web_im/security/ir.model.access.csv new file mode 100644 index 00000000000..4350b6a5941 --- /dev/null +++ b/addons/web_im/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_im_message,im.message,model_im_message,,1,1,1,1 \ No newline at end of file From 5a57546a1c696e971d921964a9a8960f5661adbe Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 16:57:25 +0100 Subject: [PATCH 017/157] better algo for moves bzr revid: nicolas.vanhoren@openerp.com-20121127155725-ee18sox7pfjz0xxo --- addons/web_im/static/src/js/im.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 1c55f6ac491..ca4cf8664dc 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -81,12 +81,11 @@ openerp.web_im = function(instance) { }); }, switch_display: function() { - var fct = _.bind(function() { - this.set("right_offset", $(window).width() - this.$el.offset().left); + var fct = _.bind(function(place) { + this.set("right_offset", place + this.$el.outerWidth()); }, this); var opt = { step: fct, - complete: fct, }; if (this.shown) { this.$el.animate({ From e7067407ddca400c46f6833588f6fc0ba5c91227 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 17:43:41 +0100 Subject: [PATCH 018/157] Improved design of button and search bzr revid: nicolas.vanhoren@openerp.com-20121127164341-ilp6wgetkr9lbdv3 --- addons/web_im/static/src/css/im.css | 44 +++++++++++++++++++++++++++-- addons/web_im/static/src/js/im.js | 23 +++++++++------ addons/web_im/static/src/xml/im.xml | 10 +++++-- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index 96180fcba9c..bed653dae9e 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -6,10 +6,50 @@ border-left: 1px solid #AEB9BD; } -.openerp .oe_topbar_imbutton{ - +.openerp .oe_topbar_imbutton { + cursor: pointer; } +/* search stuff */ +.openerp .oe_im_frame_header { + background: #dedede; + background: -moz-linear-gradient(#fcfcfc, #dedede); + background: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + border-bottom: 1px solid border-color !important; + padding: 5px; +} +.openerp .oe_im_frame_header .oe_im_searchbox { + width: 168px; + padding: 1px 21px 1px 19px; + font-size: 13px; + -moz-border-radius: 13px; + -webkit-border-radius: 13px; + border-radius: 13px; +} +.openerp .oe_im_frame_header .oe_im_search_icon { + position: absolute; + color: #888; + top: 2px; + left: 9px; + font-size: 28px; + font-family: "entypoRegular" !important; + font-weight: 300 !important; +} +.openerp .oe_im_frame_header .oe_im_search_clear { + display: none; + position: absolute; + right: 11px; + top: 4px; + font-size: 26px; + color: #b6b6b6; + cursor: pointer; +} +.openerp .oe_im_frame_header .oe_im_search_clear:hover { + color: #888; +} + + + .openerp .oe_im_conversation { position: fixed; bottom: 0px; diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index ca4cf8664dc..143e183d1c5 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -21,7 +21,7 @@ openerp.web_im = function(instance) { instance.web_im.ImTopButton = instance.web.Widget.extend({ template:'ImTopButton', events: { - "click button": "clicked", + "click .oe_e": "clicked", }, clicked: function() { this.trigger("clicked"); @@ -31,18 +31,22 @@ openerp.web_im = function(instance) { instance.web_im.InstantMessaging = instance.web.Widget.extend({ template: "InstantMessaging", events: { - "keydown .oe_im_input": "search_users", + "keydown .oe_im_searchbox": "input_change", + "keyup .oe_im_searchbox": "input_change", + "change .oe_im_searchbox": "input_change", }, init: function(parent) { this._super(parent); this.shown = false; this.set("right_offset", 0); + this.set("current_search", ""); this.last = null; this.users = []; this.c_manager = new instance.web_im.ConversationManager(this); this.on("change:right_offset", this.c_manager, _.bind(function() { this.c_manager.set("right_offset", this.get("right_offset")); }, this)); + this.user_search_dm = new instance.web.DropMisordered(); }, start: function() { this.$el.css("right", -this.$el.outerWidth()); @@ -51,7 +55,8 @@ openerp.web_im = function(instance) { this.calc_box(); this.poll(); - this.search_users(); + this.on("change:current_search", this, this.search_changed); + this.search_changed(); }, calc_box: function() { var $topbar = instance.client.$(".oe_topbar"); @@ -60,13 +65,15 @@ openerp.web_im = function(instance) { this.$el.css("top", top); this.$el.css("bottom", 0); }, - search_users: function(e) { - if(e && e.which !== 13) { - return; - } + input_change: function() { + this.set("current_search", this.$(".oe_im_searchbox").val()); + }, + search_changed: function(e) { var users = new instance.web.Model("res.users"); var self = this; - return users.query(["name"]).filter([["name", "ilike", this.$(".oe_im_input").val()]]).limit(USERS_LIMIT).all().then(function(result) { + return this.user_search_dm.add(users.query(["name"]) + .filter([["name", "ilike", this.get("current_search")]]) + .limit(USERS_LIMIT).all()).then(function(result) { self.$(".oe_im_input").val(""); _.each(self.users, function(user) { user.destroy(); diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index c54e9b85869..fd5cd13ee05 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -4,14 +4,18 @@
- +
+ ô + + [ +
-
- +
+ +
From 39ed7b144d74463323ed0eb7346a9fa0e824d2d5 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 17:44:32 +0100 Subject: [PATCH 019/157] Fixed problem with button. bzr revid: nicolas.vanhoren@openerp.com-20121127164432-jeo0us54m99etzij --- addons/web_im/static/src/js/im.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 143e183d1c5..d6e1142c1d6 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -21,7 +21,7 @@ openerp.web_im = function(instance) { instance.web_im.ImTopButton = instance.web.Widget.extend({ template:'ImTopButton', events: { - "click .oe_e": "clicked", + "click": "clicked", }, clicked: function() { this.trigger("clicked"); From 60b54200947bf6925098381f15099f67f59ccafc Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 27 Nov 2012 18:22:48 +0100 Subject: [PATCH 020/157] Made the users nice bzr revid: nicolas.vanhoren@openerp.com-20121127172248-l737z0vebm9c3y0g --- addons/web_im/static/src/css/im.css | 90 +++++++++++++----- .../web_im/static/src/img/avatar/avatar.jpeg | Bin 0 -> 6462 bytes addons/web_im/static/src/img/button-gloss.png | Bin 0 -> 74 bytes .../src/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../static/src/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes addons/web_im/static/src/img/green.png | Bin 0 -> 830 bytes addons/web_im/static/src/img/logo.png | Bin 0 -> 3600 bytes addons/web_im/static/src/img/wood.png | Bin 0 -> 102184 bytes addons/web_im/static/src/xml/im.xml | 5 +- 9 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 addons/web_im/static/src/img/avatar/avatar.jpeg create mode 100755 addons/web_im/static/src/img/button-gloss.png create mode 100755 addons/web_im/static/src/img/glyphicons-halflings-white.png create mode 100755 addons/web_im/static/src/img/glyphicons-halflings.png create mode 100644 addons/web_im/static/src/img/green.png create mode 100644 addons/web_im/static/src/img/logo.png create mode 100644 addons/web_im/static/src/img/wood.png diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index bed653dae9e..c802037236a 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -6,49 +6,87 @@ border-left: 1px solid #AEB9BD; } +/* button */ + .openerp .oe_topbar_imbutton { cursor: pointer; } /* search stuff */ .openerp .oe_im_frame_header { - background: #dedede; - background: -moz-linear-gradient(#fcfcfc, #dedede); - background: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); - border-bottom: 1px solid border-color !important; - padding: 5px; + position: relative; + background: #dedede; + background: -moz-linear-gradient(#fcfcfc, #dedede); + background: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + border-bottom: 1px solid border-color !important; + padding: 5px; } .openerp .oe_im_frame_header .oe_im_searchbox { - width: 168px; - padding: 1px 21px 1px 19px; - font-size: 13px; - -moz-border-radius: 13px; - -webkit-border-radius: 13px; - border-radius: 13px; + width: 168px; + padding: 1px 21px 1px 19px; + font-size: 13px; + -moz-border-radius: 13px; + -webkit-border-radius: 13px; + border-radius: 13px; } .openerp .oe_im_frame_header .oe_im_search_icon { - position: absolute; - color: #888; - top: 2px; - left: 9px; - font-size: 28px; - font-family: "entypoRegular" !important; - font-weight: 300 !important; + position: absolute; + color: #888; + top: 2px; + left: 9px; + font-size: 28px; + font-family: "entypoRegular" !important; + font-weight: 300 !important; } .openerp .oe_im_frame_header .oe_im_search_clear { - display: none; - position: absolute; - right: 11px; - top: 4px; - font-size: 26px; - color: #b6b6b6; - cursor: pointer; + display: none; + position: absolute; + right: 11px; + top: 4px; + font-size: 26px; + color: #b6b6b6; + cursor: pointer; } .openerp .oe_im_frame_header .oe_im_search_clear:hover { - color: #888; + color: #888; } +/* users */ +.openerp .oe_im_users { + padding-bottom: 38px; +} +.openerp .oe_im_user { + padding: 2px 6px; + cursor: pointer; + font-size: 13px; + margin-bottom: 3px; +} +.openerp .oe_im_user:hover { + background: lightGrey; +} +.openerp .oe_im_user_clip { + display: inline-block; + width: 26px; + height: 26px; + margin-right: 4px; + -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); +} +.openerp .oe_im_user_avatar { + width: 26px; + height: auto; +} +.openerp .oe_im_user_name { + width: 162px; + line-height: 26px; + padding-right: 15px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + position: relative; +} .openerp .oe_im_conversation { position: fixed; diff --git a/addons/web_im/static/src/img/avatar/avatar.jpeg b/addons/web_im/static/src/img/avatar/avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7168794022e9eba1180b9077e938fd02d722e7d8 GIT binary patch literal 6462 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 literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/img/button-gloss.png b/addons/web_im/static/src/img/button-gloss.png new file mode 100755 index 0000000000000000000000000000000000000000..6f3957702fec30ffa4a33eaf30012e06525ca9c4 GIT binary patch literal 74 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 B7G?kd literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/img/glyphicons-halflings.png b/addons/web_im/static/src/img/glyphicons-halflings.png new file mode 100755 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/img/green.png b/addons/web_im/static/src/img/green.png new file mode 100644 index 0000000000000000000000000000000000000000..01fb373c251cb73e1d2c65ea0b18ee64b40f6302 GIT binary patch literal 830 zcmV-E1Ht@>P)MUJT8>b9gTq?~7${q@YpogrN zF*KdAvRsK4vyF%>D)cdWlb|Q({yX155f**$<9wX&|Ko7axomM+tC?u67PM(j!JOt0 zk_qZ*k7n$DwNYYWnQleNwnk%jX}e`=Z)f5>K|Sr!j57a})2o>w%_+9*J-=?Q@n+sa z=TMn^WugYeIo{JA%_u|JzfPus{jznwd7vnGVWbRQ9PwvzGT;rQ!|P9jXD}7@{V9-oldzv>C`%5xjBdEFb;XqPW|F+; zmI{tO6D0p?NC7jXp#;9h!|gZ0c_$9`-WcreQNq!aD3e1jqh~5UvvS_mYr+28(I_8K z!ak%%)jiHI=e`=1ytf5ZIM5%BeZDBTd=`*HE=zZa$;D@5=G_D4P^jvG63#Id+>>e? ze54^ly<=2`%3&oc29bWMQ-R4H#bCf+j~q4H50~=T>nZ3?>jvjXO`nA$NN@~`WHG# zk2R3S46xlWEs#TQ=ovHIVo70#X0~?36s+Z^HE`kBM*~jG#o)xO5se>~!}CUuL(jB0 z^w1P6XxGe=^ZVCcTgJkzRpObQUE1$;ld-b*4VPw&X!^qW%=`BSG`u#Vb|O~ZaY_3< z#U^&F+QuT-AEk)3vZVF9#H+cdqrTdF`k*v!kUi5z5C_K$as}VZX^Z-nSR!86TUZu% zR2+d5Dv_fOQ?evWuF#ZfQv|Y)M|{ffjQl}lH${9&DOU`ew+d~FWg$6^vy?3=ffK$| z#WcnZY;9bYuql4Cuswd0P#l{fY|*b_8^a=*k!O~1mi&@_0vhppf`ieE;{X5v07*qo IM6N<$g6mO}LI3~& literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/img/logo.png b/addons/web_im/static/src/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aca5f4c60d83a9dbad64173ebd22120bb4cca615 GIT binary patch literal 3600 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

m#q8IL@8DbXsvcm#kZv*<@x*))M$hQMUWOY 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%0D3k?FK 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 F{{?f$si^<} literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index fd5cd13ee05..cb42e8c7fe8 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -20,7 +20,10 @@

From aafa81f2354acc290067aa3962786a7207e313b5 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 11:13:18 +0100 Subject: [PATCH 021/157] Adapted to new api about context, also added warning if server does not support gevent bzr revid: nicolas.vanhoren@openerp.com-20121130101318-a4xb8wx0him7f2o2 --- addons/web_im/im.py | 5 ++++- addons/web_im/static/src/js/im.js | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 4667827d979..7c791e3729f 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -95,7 +95,7 @@ class ImportController(openerp.addons.web.http.Controller): raise Exception("Not usable in a server not running gevent") num = 0 while True: - res = req.session.model('im.message').get_messages(last, req.session.eval_context(req.context)) + res = req.session.model('im.message').get_messages(last, req.context) if num >= 1 or len(res["res"]) > 0: return res last = res["last"] @@ -127,3 +127,6 @@ class im_message(osv.osv): cr.execute("notify received_message") cr.commit() return False + + def activated(self, cr, uid, context=None): + return not not openerp.tools.config.options["gevent"] diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index d6e1142c1d6..a9cd9b9d683 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -42,6 +42,7 @@ openerp.web_im = function(instance) { this.set("current_search", ""); this.last = null; this.users = []; + this.activated = false; this.c_manager = new instance.web_im.ConversationManager(this); this.on("change:right_offset", this.c_manager, _.bind(function() { this.c_manager.set("right_offset", this.get("right_offset")); @@ -54,9 +55,17 @@ openerp.web_im = function(instance) { $(window).resize(_.bind(this.calc_box, this)); this.calc_box(); - this.poll(); this.on("change:current_search", this, this.search_changed); this.search_changed(); + + var self = this; + + new instance.web.Model("im.message").call("activated", [], {context: new instance.web.CompoundContext()}).then(function(activated) { + if (activated) { + self.activated = true; + self.poll(); + } + }); }, calc_box: function() { var $topbar = instance.client.$(".oe_topbar"); @@ -99,6 +108,10 @@ openerp.web_im = function(instance) { right: -this.$el.outerWidth(), }, opt); } else { + if (! this.activated) { + this.do_warn("Instant Messaging is not activated on this server.", ""); + return; + } this.$el.animate({ right: 0, }, opt); @@ -109,7 +122,7 @@ openerp.web_im = function(instance) { var self = this; this.rpc("/im/poll", { last: this.last, - context: new instance.web.CompoundContext() + context: instance.web.pyeval.eval('context', {}), }, {shadow: true}).then(function(result) { self.last = result.last; _.each(result.res, function(mes) { From 36bec4a93e1218b00928255946fefcc9ca787768 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 14:38:09 +0100 Subject: [PATCH 022/157] Added style to conversation bzr revid: nicolas.vanhoren@openerp.com-20121130133809-14zu6sd50nbcjx0i --- addons/web_im/static/src/css/im.css | 82 ++++++++++++++++++++++++++--- addons/web_im/static/src/js/im.js | 4 +- addons/web_im/static/src/xml/im.xml | 15 ++++-- 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index c802037236a..cec95a12e33 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -88,11 +88,81 @@ position: relative; } -.openerp .oe_im_conversation { +/* conversations */ + +.openerp .oe_im_chatview { position: fixed; - bottom: 0px; - background-color: #E8EBEF; - margin: 10px; - width: 180px; + bottom: 6px; + margin-right: 5px; + background: rgba(0, 0, 0, 0.6); + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-box-shadow: 0 0 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.3); + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); + width: 240px; +} + +.openerp .oe_im_chatview .oe_im_chatview_disconnected { + display:none; +} + +.openerp .oe_im_chatview .oe_im_chatview_header { + padding: 3px 6px 2px; + background: #DEDEDE; + background: -moz-linear-gradient(#FCFCFC, #DEDEDE); + background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); + -moz-border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; + border-bottom: 1px solid #AEB9BD; + cursor: pointer; +} + +.openerp .oe_im_chatview .oe_im_chatview_close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + font-size: 18px; + line-height: 16px; + float: right; + font-weight: bold; + color: black; + text-shadow: 0 1px 0 white; + opacity: 0.2; +} + +.openerp .oe_im_chatview .oe_im_chatview_content { + overflow: auto; + height: 287px; +} + +.openerp .oe_im_chatview .oe_im_chatview_footer { + position: relative; + padding: 3px; + border-top: 1px solid #AEB9BD; + background: #DEDEDE; + background: -moz-linear-gradient(#FCFCFC, #DEDEDE); + background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); + -moz-border-radius: 0 0 3px 3px; + -webkit-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} + +.openerp .oe_im_chatview .oe_im_chatview_input { + width: 222px; + font-family: Lato, Helvetica, sans-serif; + font-size: 13px; + color: #333; + padding: 1px 5px; border: 1px solid #AEB9BD; -} \ No newline at end of file + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); +} diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index a9cd9b9d683..434fe7d16db 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -207,7 +207,7 @@ openerp.web_im = function(instance) { this.$el.css("right", this.get("right_position")); }, received_message: function(message) { - this.$(".oe_conversation_text").append($("
").text("Him: " + message.message)); + this.$(".oe_im_chatview_content").append($("
").text("Him: " + message.message)); }, send_message: function(e) { if(e && e.which !== 13) { @@ -215,7 +215,7 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this.$(".oe_conversation_text").append($("
").text("Me: " + mes)); + this.$(".oe_im_chatview_content").append($("
").text("Me: " + mes)); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); }, diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index cb42e8c7fe8..d22b4202c92 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -27,9 +27,18 @@
-
-
- +
+
+ + +
+
+ +
+
+
\ No newline at end of file From 78d5829642f5c7b15bf1695bbc3b4564a0d0fa21 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 15:16:46 +0100 Subject: [PATCH 023/157] Added style to bubble bzr revid: nicolas.vanhoren@openerp.com-20121130141646-695rr2zjfkniqrrx --- addons/web_im/static/src/css/im.css | 59 ++++++++++++++++++++++++++--- addons/web_im/static/src/js/im.js | 13 ++++++- addons/web_im/static/src/xml/im.xml | 14 +++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index cec95a12e33..c77b874d88a 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -103,11 +103,9 @@ box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); width: 240px; } - .openerp .oe_im_chatview .oe_im_chatview_disconnected { display:none; } - .openerp .oe_im_chatview .oe_im_chatview_header { padding: 3px 6px 2px; background: #DEDEDE; @@ -119,7 +117,6 @@ border-bottom: 1px solid #AEB9BD; cursor: pointer; } - .openerp .oe_im_chatview .oe_im_chatview_close { padding: 0; cursor: pointer; @@ -134,12 +131,10 @@ text-shadow: 0 1px 0 white; opacity: 0.2; } - .openerp .oe_im_chatview .oe_im_chatview_content { overflow: auto; height: 287px; } - .openerp .oe_im_chatview .oe_im_chatview_footer { position: relative; padding: 3px; @@ -151,7 +146,6 @@ -webkit-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } - .openerp .oe_im_chatview .oe_im_chatview_input { width: 222px; font-family: Lato, Helvetica, sans-serif; @@ -166,3 +160,56 @@ -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); } +.openerp .oe_im_chatview .oe_im_chatview_bubble { + background: white; + padding: 3px; + margin: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.openerp .oe_im_chatview .oe_im_chatview_clip { + position: relative; + float: left; + width: 26px; + height: 26px; + margin-right: 4px; + -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); +} +.openerp .oe_im_chatview .oe_im_chatview_avatar { + float: left; + width: 26px; + height: auto; + clip: rect(0, 26px, 26px, 0); + max-width: 100%; + width: auto 9; + height: auto; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} +.openerp .oe_im_chatview .oe_im_chatview_time { + text-align: right; + line-height: 13px; + font-size: 11px; + color: #999; +} +.openerp .oe_im_chatview .oe_im_chatview_from { + margin: 0 0 2px 0; + line-height: 14px; + font-weight: bold; + font-size: 12px; + width: 154px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: #3A87AD; +} +.openerp .oe_im_chatview .oe_im_chatview_bubble_list { +} +.openerp .oe_im_chatview .oe_im_chatview_bubble_item { + margin: 0 0 2px 30px; + line-height: 14px; +} diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 434fe7d16db..a7ad0fa36e7 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -4,6 +4,10 @@ openerp.web_im = function(instance) { var USERS_LIMIT = 20; var ERROR_DELAY = 5000; + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + instance.web.UserMenu.include({ do_update: function(){ var self = this; @@ -207,7 +211,7 @@ openerp.web_im = function(instance) { this.$el.css("right", this.get("right_position")); }, received_message: function(message) { - this.$(".oe_im_chatview_content").append($("
").text("Him: " + message.message)); + this._add_bubble("Him", [message.message]); }, send_message: function(e) { if(e && e.which !== 13) { @@ -215,10 +219,15 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this.$(".oe_im_chatview_content").append($("
").text("Me: " + mes)); + this._add_bubble("Me", [mes]); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); }, + _add_bubble: function(name, items) { + var date = new Date().toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); + var bubble = QWeb.render("Conversation.bubble", {"items": items, "name": name, "time": date}); + this.$(".oe_im_chatview_content").append($(bubble)); + }, destroy: function() { this.trigger("destroyed"); return this._super(); diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index d22b4202c92..2f7d450db35 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -41,4 +41,18 @@
+ +
+
+ +
+
+
+ +
+
+
+
+
+
\ No newline at end of file From 88f6d86264961c66c9c22ee8c1883bccecf6a8b3 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 15:26:01 +0100 Subject: [PATCH 024/157] Added date on message bzr revid: nicolas.vanhoren@openerp.com-20121130142601-bsqe00tyv2thuqz4 --- addons/web_im/im.py | 8 +++++++- addons/web_im/static/src/js/im.js | 9 +++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 7c791e3729f..ab4bacd7905 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -22,6 +22,7 @@ import openerp import openerp.tools.config import openerp.modules.registry +import datetime from osv import osv, fields WATCHER_TIMER = 60 @@ -109,6 +110,11 @@ class im_message(osv.osv): 'message': fields.char(string="Message", size=200, required=True), 'from': fields.many2one("res.users", "From", required= True, ondelete='cascade'), 'to': fields.many2one("res.users", "From", required=True, select=True, ondelete='cascade'), + 'date': fields.datetime("Date", required=True), + } + + _defaults = { + 'date': datetime.datetime.now(), } def get_messages(self, cr, uid, last=None, context=None): @@ -116,7 +122,7 @@ class im_message(osv.osv): tmp = self.search(cr, uid, [['to', '=', uid]], order="id desc", limit=1, context=context) last = tmp[0] if len(tmp) >= 1 else -1 res = self.search(cr, uid, [['id', '>', last], ['to', '=', uid]], order="id", context=context) - res = self.read(cr, uid, res, ["id", "message", "from"], context=context) + res = self.read(cr, uid, res, ["id", "message", "from", "date"], context=context) if len(res) > 0: last = res[-1]["id"] return {"res": res, "last": last, "dbname": cr.dbname} diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index a7ad0fa36e7..7e45c4db17a 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -211,7 +211,7 @@ openerp.web_im = function(instance) { this.$el.css("right", this.get("right_position")); }, received_message: function(message) { - this._add_bubble("Him", [message.message]); + this._add_bubble("Him", [message.message], message.date); }, send_message: function(e) { if(e && e.which !== 13) { @@ -219,12 +219,13 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this._add_bubble("Me", [mes]); + this._add_bubble("Me", [mes], instance.web.datetime_to_str(new Date())); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); }, - _add_bubble: function(name, items) { - var date = new Date().toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); + _add_bubble: function(name, items, date) { + date = instance.web.str_to_datetime(date); + date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); var bubble = QWeb.render("Conversation.bubble", {"items": items, "name": name, "time": date}); this.$(".oe_im_chatview_content").append($(bubble)); }, From 051fcf50b7b87b8c183c98f3953f882eaf759f43 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 15:33:11 +0100 Subject: [PATCH 025/157] Added close button on conversations bzr revid: nicolas.vanhoren@openerp.com-20121130143311-imipvgtrl99iejo2 --- addons/web_im/static/src/js/im.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 7e45c4db17a..b811aea9593 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -197,6 +197,7 @@ openerp.web_im = function(instance) { "template": "Conversation", events: { "keydown input": "send_message", + "click .oe_im_chatview_close": "destroy", }, init: function(parent, user_rec) { this._super(parent); From c555f34f879e3b141ca6bbc542b65b3fa6832112 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 15:54:36 +0100 Subject: [PATCH 026/157] Allow message to be received when deconnected bzr revid: nicolas.vanhoren@openerp.com-20121130145436-7qaat0v8g816nlxm --- addons/web_im/im.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index ab4bacd7905..920e3423520 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -118,9 +118,16 @@ class im_message(osv.osv): } def get_messages(self, cr, uid, last=None, context=None): - if not last: - tmp = self.search(cr, uid, [['to', '=', uid]], order="id desc", limit=1, context=context) - last = tmp[0] if len(tmp) >= 1 else -1 + # complex stuff to determine the last message to show + users = self.pool.get("res.users") + c_user = users.browse(cr, uid, uid, context=context) + if last: + print "here: ", c_user.im_last_received + if c_user.im_last_received < last: + users.write(cr, openerp.SUPERUSER_ID, uid, {'im_last_received': last}, context=context) + else: + last = c_user.im_last_received or -1 + res = self.search(cr, uid, [['id', '>', last], ['to', '=', uid]], order="id", context=context) res = self.read(cr, uid, res, ["id", "message", "from", "date"], context=context) if len(res) > 0: @@ -136,3 +143,10 @@ class im_message(osv.osv): def activated(self, cr, uid, context=None): return not not openerp.tools.config.options["gevent"] + +class res_user(osv.osv): + _inherit = "res.users" + + _columns = { + 'im_last_received': fields.integer(string="Last Received Message"), + } From 2f5faf419d7dabf4fcef1b351e68a947011773de Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 16:03:13 +0100 Subject: [PATCH 027/157] Added scroll in chat bzr revid: nicolas.vanhoren@openerp.com-20121130150313-rrh17i2yo2tbvilu --- addons/web_im/static/src/js/im.js | 3 ++- addons/web_im/static/src/xml/im.xml | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index b811aea9593..a709a727334 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -228,7 +228,8 @@ openerp.web_im = function(instance) { date = instance.web.str_to_datetime(date); date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); var bubble = QWeb.render("Conversation.bubble", {"items": items, "name": name, "time": date}); - this.$(".oe_im_chatview_content").append($(bubble)); + $(this.$(".oe_im_chatview_content").children()[0]).append($(bubble)); + this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); }, destroy: function() { this.trigger("destroyed"); diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 2f7d450db35..38348d5952b 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -35,7 +35,9 @@
-
+
+
+
From a7274b21e796ecc2caf0449edc69ad3c2169e9f7 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 16:07:48 +0100 Subject: [PATCH 028/157] Fixed margin problems in conversations bzr revid: nicolas.vanhoren@openerp.com-20121130150748-ui2ihz8qhrezczu9 --- addons/web_im/static/src/css/im.css | 2 +- addons/web_im/static/src/js/im.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index c77b874d88a..2da477748cd 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -93,7 +93,7 @@ .openerp .oe_im_chatview { position: fixed; bottom: 6px; - margin-right: 5px; + margin-right: 6px; background: rgba(0, 0, 0, 0.6); -moz-border-radius: 3px; -webkit-border-radius: 3px; diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index a709a727334..bba8ae4d27a 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -188,7 +188,7 @@ openerp.web_im = function(instance) { var current = this.get("right_offset"); _.each(_.range(this.conversations.length), function(i) { this.conversations[i].set("right_position", current); - current += this.conversations[i].$el.outerWidth(); + current += this.conversations[i].$el.outerWidth(true); }, this); }, }); From 23f8d7e5c9859899afeea84bbf4cb1f5d3f25bfe Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 16:09:12 +0100 Subject: [PATCH 029/157] minor fix bzr revid: nicolas.vanhoren@openerp.com-20121130150912-wp0edl27fpvolibh --- addons/web_im/static/src/js/im.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index bba8ae4d27a..554c2c728c5 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -174,6 +174,7 @@ openerp.web_im = function(instance) { conv.on("destroyed", this, function() { this.conversations = _.without(this.conversations, conv); delete this.users[conv.user_rec.id]; + this.calc_positions(); }); this.conversations.push(conv); this.users[user_rec.id] = conv; From d4c72c72e55a1a83e016ef7ce597d5df5f5088ca Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 16:34:21 +0100 Subject: [PATCH 030/157] Added users cache bzr revid: nicolas.vanhoren@openerp.com-20121130153421-2fxtmib03hv32o90 --- addons/web_im/static/src/js/im.js | 40 ++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 554c2c728c5..8445f21960a 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -52,6 +52,7 @@ openerp.web_im = function(instance) { this.c_manager.set("right_offset", this.get("right_offset")); }, this)); this.user_search_dm = new instance.web.DropMisordered(); + this.users_cache = {}; }, start: function() { this.$el.css("right", -this.$el.outerWidth()); @@ -84,9 +85,12 @@ openerp.web_im = function(instance) { search_changed: function(e) { var users = new instance.web.Model("res.users"); var self = this; - return this.user_search_dm.add(users.query(["name"]) + return this.user_search_dm.add(users.query(["name", "image"]) .filter([["name", "ilike", this.get("current_search")]]) .limit(USERS_LIMIT).all()).then(function(result) { + _.each(result, function(user) { + self.users_cache[user.id] = user; + }); self.$(".oe_im_input").val(""); _.each(self.users, function(user) { user.destroy(); @@ -100,6 +104,26 @@ openerp.web_im = function(instance) { }); }); }, + ensure_users: function(user_ids) { + var no_cache = {}; + _.each(user_ids, function(el) { + if (! this.users_cache[el]) + no_cache[el] = el; + }, this); + var self = this; + if (_.size(no_cache) === 0) + return $.when(); + else + return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name", "image"]], + {context: new instance.web.CompoundContext()}).then(function(users) { + _.each(users, function(el) { + self.users_cache[el.id] = el; + }); + }); + }, + get_user: function(user_id) { + return this.users_cache[user_id]; + }, switch_display: function() { var fct = _.bind(function(place) { this.set("right_offset", place + this.$el.outerWidth()); @@ -129,10 +153,14 @@ openerp.web_im = function(instance) { context: instance.web.pyeval.eval('context', {}), }, {shadow: true}).then(function(result) { self.last = result.last; - _.each(result.res, function(mes) { - self.c_manager.received_message(mes, {"id": mes.from[0]}); + var user_ids = _.pluck(_.pluck(result.res, "from"), 0); + self.ensure_users(user_ids).then(function() { + _.each(result.res, function(mes) { + mes.from = self.get_user(mes.from[0]); + self.c_manager.received_message(mes); + }); + self.poll(); }); - self.poll(); }, function(unused, e) { e.preventDefault(); setTimeout(_.bind(self.poll, self), ERROR_DELAY); @@ -181,8 +209,8 @@ openerp.web_im = function(instance) { this.calc_positions(); return conv; }, - received_message: function(message, user_rec) { - var conv = this.activate_user(user_rec); + received_message: function(message) { + var conv = this.activate_user(message.from); conv.received_message(message); }, calc_positions: function() { From c7784a9fbd70c6a4c5d9d3f47a846ba745a18136 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 17:15:59 +0100 Subject: [PATCH 031/157] Replaced user_rec by real objects bzr revid: nicolas.vanhoren@openerp.com-20121130161559-o3auhz36iwyigbvo --- addons/web_im/static/src/js/im.js | 97 ++++++++++++++++++++--------- addons/web_im/static/src/xml/im.xml | 8 +-- 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 8445f21960a..bd69cfea9cf 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -88,20 +88,19 @@ openerp.web_im = function(instance) { return this.user_search_dm.add(users.query(["name", "image"]) .filter([["name", "ilike", this.get("current_search")]]) .limit(USERS_LIMIT).all()).then(function(result) { - _.each(result, function(user) { - self.users_cache[user.id] = user; - }); + self.add_to_user_cache(result); self.$(".oe_im_input").val(""); - _.each(self.users, function(user) { - user.destroy(); - }); + var old_users = self.users; self.users = []; _.each(result, function(user) { - var widget = new instance.web_im.ImUser(self, user); + var widget = new instance.web_im.UserWidget(self, self.get_user(user.id)); widget.appendTo(self.$(".oe_im_users")); widget.on("activate_user", self, self.activate_user); self.users.push(widget); }); + _.each(old_users, function(user) { + user.destroy(); + }); }); }, ensure_users: function(user_ids) { @@ -116,11 +115,20 @@ openerp.web_im = function(instance) { else return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name", "image"]], {context: new instance.web.CompoundContext()}).then(function(users) { - _.each(users, function(el) { - self.users_cache[el.id] = el; - }); + self.add_to_user_cache(users); }); }, + add_to_user_cache: function(user_recs) { + _.each(user_recs, function(user_rec) { + if (! this.users_cache[user_rec.id]) { + var user = new instance.web_im.ImUser(this, user_rec); + this.users_cache[user_rec.id] = user; + user.on("destroyed", this, function() { + delete this.users_cache[user_rec.id]; + }); + } + }, this); + }, get_user: function(user_id) { return this.users_cache[user_id]; }, @@ -156,8 +164,8 @@ openerp.web_im = function(instance) { var user_ids = _.pluck(_.pluck(result.res, "from"), 0); self.ensure_users(user_ids).then(function() { _.each(result.res, function(mes) { - mes.from = self.get_user(mes.from[0]); - self.c_manager.received_message(mes); + var user = self.get_user(mes.from[0]); + self.c_manager.received_message(mes, user); }); self.poll(); }); @@ -166,22 +174,49 @@ openerp.web_im = function(instance) { setTimeout(_.bind(self.poll, self), ERROR_DELAY); }); }, - activate_user: function(user_rec) { - this.c_manager.activate_user(user_rec); + activate_user: function(user) { + this.c_manager.activate_user(user); }, }); - instance.web_im.ImUser = instance.web.Widget.extend({ - "template": "ImUser", + instance.web_im.UserWidget = instance.web.Widget.extend({ + "template": "UserWidget", events: { "click": "activate_user", }, - init: function(parent, user_rec) { + init: function(parent, user) { this._super(parent); - this.user_rec = user_rec; + this.user = user; + this.user.add_watcher(); }, activate_user: function() { - this.trigger("activate_user", this.user_rec); + this.trigger("activate_user", this.user); + }, + destroy: function() { + this.user.remove_watcher(); + this._super(); + }, + }); + + instance.web_im.ImUser = instance.web.Class.extend(instance.web.PropertiesMixin, { + init: function(parent, user_rec) { + instance.web.PropertiesMixin.init.call(this, parent); + this.set(user_rec); + this.set("watcher_count", 0); + this.on("change:watcher_count", this, function() { + if (this.get("watcher_count") === 0) + this.destroy(); + }); + }, + destroy: function() { + this.trigger("destroyed"); + instance.web.PropertiesMixin.destroy.call(this); + }, + add_watcher: function() { + this.set("watcher_count", this.get("watcher_count") + 1); + }, + remove_watcher: function() { + this.set("watcher_count", this.get("watcher_count") - 1); }, }); @@ -193,24 +228,24 @@ openerp.web_im = function(instance) { this.users = {}; this.on("change:right_offset", this, this.calc_positions); }, - activate_user: function(user_rec) { - if (this.users[user_rec.id]) { - return this.users[user_rec.id]; + activate_user: function(user) { + if (this.users[user.get('id')]) { + return this.users[user.get('id')]; } - var conv = new instance.web_im.Conversation(this, user_rec); + var conv = new instance.web_im.Conversation(this, user); conv.appendTo(instance.client.$el); conv.on("destroyed", this, function() { this.conversations = _.without(this.conversations, conv); - delete this.users[conv.user_rec.id]; + delete this.users[conv.user.get('id')]; this.calc_positions(); }); this.conversations.push(conv); - this.users[user_rec.id] = conv; + this.users[user.get('id')] = conv; this.calc_positions(); return conv; }, - received_message: function(message) { - var conv = this.activate_user(message.from); + received_message: function(message, user) { + var conv = this.activate_user(user); conv.received_message(message); }, calc_positions: function() { @@ -228,9 +263,10 @@ openerp.web_im = function(instance) { "keydown input": "send_message", "click .oe_im_chatview_close": "destroy", }, - init: function(parent, user_rec) { + init: function(parent, user) { this._super(parent); - this.user_rec = user_rec; + this.user = user; + this.user.add_watcher(); this.set("right_position", 0); }, start: function() { @@ -251,7 +287,7 @@ openerp.web_im = function(instance) { this.$("input").val(""); this._add_bubble("Me", [mes], instance.web.datetime_to_str(new Date())); var model = new instance.web.Model("im.message"); - model.call("post", [mes, this.user_rec.id], {context: new instance.web.CompoundContext()}); + model.call("post", [mes, this.user.get('id')], {context: new instance.web.CompoundContext()}); }, _add_bubble: function(name, items, date) { date = instance.web.str_to_datetime(date); @@ -261,6 +297,7 @@ openerp.web_im = function(instance) { this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); }, destroy: function() { + this.user.remove_watcher(); this.trigger("destroyed"); return this._super(); }, diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 38348d5952b..1e4d648cf9d 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -18,22 +18,22 @@ +
- +
- +
- +
- +
From 3ded9e15122053d1ce76bbe48ec7dd685388ac31 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 17:26:39 +0100 Subject: [PATCH 032/157] Added real images bzr revid: nicolas.vanhoren@openerp.com-20121130162639-sr7w83l6u1zvs32r --- addons/web_im/static/src/js/im.js | 13 +++++++------ addons/web_im/static/src/xml/im.xml | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index bd69cfea9cf..676175fdf70 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -85,7 +85,7 @@ openerp.web_im = function(instance) { search_changed: function(e) { var users = new instance.web.Model("res.users"); var self = this; - return this.user_search_dm.add(users.query(["name", "image"]) + return this.user_search_dm.add(users.query(["name"]) .filter([["name", "ilike", this.get("current_search")]]) .limit(USERS_LIMIT).all()).then(function(result) { self.add_to_user_cache(result); @@ -113,7 +113,7 @@ openerp.web_im = function(instance) { if (_.size(no_cache) === 0) return $.when(); else - return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name", "image"]], + return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name"]], {context: new instance.web.CompoundContext()}).then(function(users) { self.add_to_user_cache(users); }); @@ -201,6 +201,7 @@ openerp.web_im = function(instance) { instance.web_im.ImUser = instance.web.Class.extend(instance.web.PropertiesMixin, { init: function(parent, user_rec) { instance.web.PropertiesMixin.init.call(this, parent); + user_rec.image_url = instance.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user_rec.id}); this.set(user_rec); this.set("watcher_count", 0); this.on("change:watcher_count", this, function() { @@ -277,7 +278,7 @@ openerp.web_im = function(instance) { this.$el.css("right", this.get("right_position")); }, received_message: function(message) { - this._add_bubble("Him", [message.message], message.date); + this._add_bubble(this.user, [message.message], message.date); }, send_message: function(e) { if(e && e.which !== 13) { @@ -285,14 +286,14 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this._add_bubble("Me", [mes], instance.web.datetime_to_str(new Date())); + this._add_bubble(this.user, [mes], instance.web.datetime_to_str(new Date())); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user.get('id')], {context: new instance.web.CompoundContext()}); }, - _add_bubble: function(name, items, date) { + _add_bubble: function(user, items, date) { date = instance.web.str_to_datetime(date); date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); - var bubble = QWeb.render("Conversation.bubble", {"items": items, "name": name, "time": date}); + var bubble = QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date}); $(this.$(".oe_im_chatview_content").children()[0]).append($(bubble)); this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); }, diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 1e4d648cf9d..72d441de053 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -21,7 +21,7 @@
- +
@@ -46,9 +46,9 @@
- +
-
+
From 47acefca47ce006b986cd49a78996839d8431408 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 17:27:38 +0100 Subject: [PATCH 033/157] Added translation bzr revid: nicolas.vanhoren@openerp.com-20121130162738-i818gq2o0d6xptk9 --- addons/web_im/static/src/xml/im.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 72d441de053..70551e3424b 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -39,7 +39,7 @@
From 68055413aa4da65830543b00fd76a96add25bca8 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 17:38:14 +0100 Subject: [PATCH 034/157] Handles correctly me bzr revid: nicolas.vanhoren@openerp.com-20121130163814-w3dl8zlps92nint7 --- addons/web_im/static/src/js/im.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 676175fdf70..9dff54f359f 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -65,11 +65,14 @@ openerp.web_im = function(instance) { var self = this; - new instance.web.Model("im.message").call("activated", [], {context: new instance.web.CompoundContext()}).then(function(activated) { - if (activated) { - self.activated = true; - self.poll(); - } + return this.ensure_users([instance.session.uid]).then(function() { + self.c_manager.set_me(self.get_user(instance.session.uid)); + return new instance.web.Model("im.message").call("activated", [], {context: new instance.web.CompoundContext()}).then(function(activated) { + if (activated) { + self.activated = true; + self.poll(); + } + }); }); }, calc_box: function() { @@ -229,11 +232,14 @@ openerp.web_im = function(instance) { this.users = {}; this.on("change:right_offset", this, this.calc_positions); }, + set_me: function(me) { + this.me = me; + }, activate_user: function(user) { if (this.users[user.get('id')]) { return this.users[user.get('id')]; } - var conv = new instance.web_im.Conversation(this, user); + var conv = new instance.web_im.Conversation(this, user, this.me); conv.appendTo(instance.client.$el); conv.on("destroyed", this, function() { this.conversations = _.without(this.conversations, conv); @@ -264,8 +270,9 @@ openerp.web_im = function(instance) { "keydown input": "send_message", "click .oe_im_chatview_close": "destroy", }, - init: function(parent, user) { + init: function(parent, user, me) { this._super(parent); + this.me = me; this.user = user; this.user.add_watcher(); this.set("right_position", 0); @@ -286,7 +293,7 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this._add_bubble(this.user, [mes], instance.web.datetime_to_str(new Date())); + this._add_bubble(this.me, [mes], instance.web.datetime_to_str(new Date())); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user.get('id')], {context: new instance.web.CompoundContext()}); }, From e50b0249b6232f1f866884ca6836dadbd97f5fe5 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 30 Nov 2012 18:02:43 +0100 Subject: [PATCH 035/157] Added multi bubble bzr revid: nicolas.vanhoren@openerp.com-20121130170243-0isooew0e5g01tz0 --- addons/web_im/static/src/js/im.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 9dff54f359f..6af20f0f131 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -285,7 +285,7 @@ openerp.web_im = function(instance) { this.$el.css("right", this.get("right_position")); }, received_message: function(message) { - this._add_bubble(this.user, [message.message], message.date); + this._add_bubble(this.user, message.message, message.date); }, send_message: function(e) { if(e && e.which !== 13) { @@ -293,15 +293,22 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this._add_bubble(this.me, [mes], instance.web.datetime_to_str(new Date())); + this._add_bubble(this.me, mes, instance.web.datetime_to_str(new Date())); var model = new instance.web.Model("im.message"); model.call("post", [mes, this.user.get('id')], {context: new instance.web.CompoundContext()}); }, - _add_bubble: function(user, items, date) { + _add_bubble: function(user, item, date) { + var items = [item]; + if (user === this.last_user) { + this.last_bubble.remove(); + items = this.last_items.concat(items); + } + this.last_user = user; + this.last_items = items; date = instance.web.str_to_datetime(date); date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); - var bubble = QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date}); - $(this.$(".oe_im_chatview_content").children()[0]).append($(bubble)); + this.last_bubble = $(QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date})); + $(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble); this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); }, destroy: function() { From 06ca46957cee7d60b68c1f86decb7b727e23c09b Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 3 Dec 2012 15:50:09 +0100 Subject: [PATCH 036/157] Added small effect on conversations bzr revid: nicolas.vanhoren@openerp.com-20121203145009-osxforhc5om0zaeu --- addons/web_im/static/src/css/im.css | 1 + addons/web_im/static/src/js/im.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index 2da477748cd..5f0ad245187 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -92,6 +92,7 @@ .openerp .oe_im_chatview { position: fixed; + overflow: hidden; bottom: 6px; margin-right: 6px; background: rgba(0, 0, 0, 0.6); diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 6af20f0f131..4651975bc31 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -269,6 +269,7 @@ openerp.web_im = function(instance) { events: { "keydown input": "send_message", "click .oe_im_chatview_close": "destroy", + "click .oe_im_chatview_header": "show_hide", }, init: function(parent, user, me) { this._super(parent); @@ -276,11 +277,25 @@ openerp.web_im = function(instance) { this.user = user; this.user.add_watcher(); this.set("right_position", 0); + this.shown = true; }, start: function() { this.on("change:right_position", this, this.calc_pos); + this.full_height = this.$el.height(); this.calc_pos(); }, + show_hide: function() { + if (this.shown) { + this.$el.animate({ + height: this.$(".oe_im_chatview_header").outerHeight(), + }); + } else { + this.$el.animate({ + height: this.full_height, + }); + } + this.shown = ! this.shown; + }, calc_pos: function() { this.$el.css("right", this.get("right_position")); }, From faba20c1095ef291fd60d805fdbcb630a235aac9 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 3 Dec 2012 16:30:20 +0100 Subject: [PATCH 037/157] Added users status + polling bzr revid: nicolas.vanhoren@openerp.com-20121203153020-yb92jj5kt7g7h6ct --- addons/web_im/im.py | 34 ++++++++++++++++++++++++++----- addons/web_im/static/src/js/im.js | 11 +++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 920e3423520..7027fe602f8 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -22,11 +22,13 @@ import openerp import openerp.tools.config import openerp.modules.registry +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT import datetime from osv import osv, fields WATCHER_TIMER = 60 POLL_TIMER = 30 +DISCONNECTION_TIMER = POLL_TIMER + 5 if openerp.tools.config.options["gevent"]: import gevent @@ -91,12 +93,12 @@ class ImportController(openerp.addons.web.http.Controller): _cp_path = '/im' @openerp.addons.web.http.jsonrequest - def poll(self, req, last=None): + def poll(self, req, last=None, users_watch=None): if not openerp.tools.config.options["gevent"]: raise Exception("Not usable in a server not running gevent") num = 0 while True: - res = req.session.model('im.message').get_messages(last, req.context) + res = req.session.model('im.message').get_messages(last, users_watch, req.context) if num >= 1 or len(res["res"]) > 0: return res last = res["last"] @@ -117,7 +119,9 @@ class im_message(osv.osv): 'date': datetime.datetime.now(), } - def get_messages(self, cr, uid, last=None, context=None): + def get_messages(self, cr, uid, last=None, users_watch=None, context=None): + users_watch = users_watch or [] + # complex stuff to determine the last message to show users = self.pool.get("res.users") c_user = users.browse(cr, uid, uid, context=context) @@ -132,7 +136,9 @@ class im_message(osv.osv): res = self.read(cr, uid, res, ["id", "message", "from", "date"], context=context) if len(res) > 0: last = res[-1]["id"] - return {"res": res, "last": last, "dbname": cr.dbname} + print "users_watch:", users_watch + users_status = users.read(cr, uid, users_watch, ["im_status"], context=context) + return {"res": res, "last": last, "dbname": cr.dbname, "users_status": users_status} def post(self, cr, uid, message, to_user_id, context=None): self.create(cr, uid, {"message": message, 'from': uid, 'to': to_user_id}, context=context) @@ -147,6 +153,24 @@ class im_message(osv.osv): class res_user(osv.osv): _inherit = "res.users" + def _im_status(self, cr, uid, ids, something, something_else, context=None): + res = {} + current = datetime.datetime.now() + delta = datetime.timedelta(0, DISCONNECTION_TIMER) + for obj in self.browse(cr, uid, ids, context=context): + last_update = datetime.datetime.strptime(obj.im_last_status_update, DEFAULT_SERVER_DATETIME_FORMAT) + res[obj.id] = obj.im_last_status and (last_update + delta) > current + return res + _columns = { - 'im_last_received': fields.integer(string="Last Received Message"), + 'im_last_received': fields.integer(string="Instant Messaging Last Received Message"), + 'im_last_status': fields.boolean(strint="Instant Messaging Last Status"), + 'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"), + 'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean'), + } + + _defaults = { + 'im_last_received': -1, + 'im_last_status': False, + 'im_last_status_update': datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT), } diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 4651975bc31..9d6ebc4a4cb 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -66,7 +66,9 @@ openerp.web_im = function(instance) { var self = this; return this.ensure_users([instance.session.uid]).then(function() { - self.c_manager.set_me(self.get_user(instance.session.uid)); + var me = self.users_cache[instance.session.uid]; + delete self.users_cache[instance.session.uid]; + self.c_manager.set_me(me); return new instance.web.Model("im.message").call("activated", [], {context: new instance.web.CompoundContext()}).then(function(activated) { if (activated) { self.activated = true; @@ -159,10 +161,17 @@ openerp.web_im = function(instance) { }, poll: function() { var self = this; + var user_ids = _.map(this.users_cache, function(el) { + return el.get("id"); + }); this.rpc("/im/poll", { last: this.last, + users_watch: user_ids, context: instance.web.pyeval.eval('context', {}), }, {shadow: true}).then(function(result) { + _.each(result.users_status, function(el) { + self.get_user(el.id).set(el); + }); self.last = result.last; var user_ids = _.pluck(_.pluck(result.res, "from"), 0); self.ensure_users(user_ids).then(function() { From 00b125ea3c669680ada89fe512fd23255f73e7c3 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 3 Dec 2012 17:17:10 +0100 Subject: [PATCH 038/157] Improved watcher bzr revid: nicolas.vanhoren@openerp.com-20121203161710-nvej55rwr2elu3l6 --- addons/web_im/im.py | 58 +++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 7027fe602f8..e7dbee5c7ad 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -25,8 +25,13 @@ import openerp.modules.registry from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT import datetime from osv import osv, fields +import time +import logging + +_logger = logging.getLogger(__name__) WATCHER_TIMER = 60 +WATCHER_ERROR_DELAY = 10 POLL_TIMER = 30 DISCONNECTION_TIMER = POLL_TIMER + 5 @@ -54,34 +59,42 @@ if openerp.tools.config.options["gevent"]: gevent.spawn(self.loop) def loop(self): - try: - while True: - if self.waiting == 0: - return + _logger.info("Begin watching for instant messaging events for database " + self.db_name) + stopping = False + while not stopping: + try: registry = openerp.modules.registry.RegistryManager.get(self.db_name) with registry.cursor() as c: conn = c._cnx try: c.execute("listen received_message;") c.commit(); - if select.select([conn], [], [], WATCHER_TIMER) == ([],[],[]): - pass - else: - conn.poll() - while conn.notifies: - notify = conn.notifies.pop() - self.posted.set() - self.posted.clear() + while not stopping: + if self.waiting == 0: + stopping = True + break + if select.select([conn], [], [], WATCHER_TIMER) == ([],[],[]): + pass + else: + conn.poll() + while conn.notifies: + notify = conn.notifies.pop() + self.posted.set() + self.posted.clear() finally: try: c.execute("unlisten received_message;") c.commit() except: pass # can't do anything if that fails - finally: - del Watcher.watchers[self.db_name] - self.posted.set() - self.posted = None + except: + # if something crash, we wait some time then try again + _logger.exception("Exception during instant messaging watcher activity") + time.sleep(WATCHER_ERROR_DELAY) + del Watcher.watchers[self.db_name] + self.posted.set() + self.posted = None + _logger.info("End watching for instant messaging events for database " + self.db_name) def stop(self, timeout=None): self.waiting += 1 @@ -96,6 +109,7 @@ class ImportController(openerp.addons.web.http.Controller): def poll(self, req, last=None, users_watch=None): if not openerp.tools.config.options["gevent"]: raise Exception("Not usable in a server not running gevent") + res = req.session.model('res.users').im_connect(context=req.context) num = 0 while True: res = req.session.model('im.message').get_messages(last, users_watch, req.context) @@ -126,7 +140,6 @@ class im_message(osv.osv): users = self.pool.get("res.users") c_user = users.browse(cr, uid, uid, context=context) if last: - print "here: ", c_user.im_last_received if c_user.im_last_received < last: users.write(cr, openerp.SUPERUSER_ID, uid, {'im_last_received': last}, context=context) else: @@ -136,7 +149,6 @@ class im_message(osv.osv): res = self.read(cr, uid, res, ["id", "message", "from", "date"], context=context) if len(res) > 0: last = res[-1]["id"] - print "users_watch:", users_watch users_status = users.read(cr, uid, users_watch, ["im_status"], context=context) return {"res": res, "last": last, "dbname": cr.dbname, "users_status": users_status} @@ -162,6 +174,16 @@ class res_user(osv.osv): res[obj.id] = obj.im_last_status and (last_update + delta) > current return res + def im_connect(self, cr, uid, context=None): + self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, + "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + return True + + def im_disconnect(self, cr, uid, context=None): + self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, + "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + return True + _columns = { 'im_last_received': fields.integer(string="Instant Messaging Last Received Message"), 'im_last_status': fields.boolean(strint="Instant Messaging Last Status"), From 3be3f619a3d4090a8dd9978be76acf19fb0ae50e Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 3 Dec 2012 17:38:42 +0100 Subject: [PATCH 039/157] Made connect/disconnect work more or less bzr revid: nicolas.vanhoren@openerp.com-20121203163842-4h8aqlhyi09be5zf --- addons/web_im/im.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index e7dbee5c7ad..e97556c7e40 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -26,7 +26,8 @@ from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT import datetime from osv import osv, fields import time -import logging +import logging +import json _logger = logging.getLogger(__name__) @@ -67,7 +68,7 @@ if openerp.tools.config.options["gevent"]: with registry.cursor() as c: conn = c._cnx try: - c.execute("listen received_message;") + c.execute("listen im_channel;") c.commit(); while not stopping: if self.waiting == 0: @@ -78,12 +79,13 @@ if openerp.tools.config.options["gevent"]: else: conn.poll() while conn.notifies: - notify = conn.notifies.pop() + notify = conn.notifies.pop().payload + # do something with it self.posted.set() self.posted.clear() finally: try: - c.execute("unlisten received_message;") + c.execute("unlisten im_channel;") c.commit() except: pass # can't do anything if that fails @@ -155,7 +157,7 @@ class im_message(osv.osv): def post(self, cr, uid, message, to_user_id, context=None): self.create(cr, uid, {"message": message, 'from': uid, 'to': to_user_id}, context=context) cr.commit() - cr.execute("notify received_message") + cr.execute("notify im_channel, %s", [json.dumps({'type': 'message', 'receiver': to_user_id})]) cr.commit() return False @@ -177,11 +179,17 @@ class res_user(osv.osv): def im_connect(self, cr, uid, context=None): self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + cr.commit() + cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) + cr.commit() return True def im_disconnect(self, cr, uid, context=None): self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + cr.commit() + cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) + cr.commit() return True _columns = { From 54c9bab185e7fd59123f70cb115f1a6ae8fda7e7 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 3 Dec 2012 18:04:12 +0100 Subject: [PATCH 040/157] Added status notification bzr revid: nicolas.vanhoren@openerp.com-20121203170412-h1fgkwa08tq6a5aw --- addons/web_im/static/src/css/im.css | 27 +++++++++++++++++++++++++++ addons/web_im/static/src/js/im.js | 18 ++++++++++++++++-- addons/web_im/static/src/xml/im.xml | 2 ++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index 5f0ad245187..51717c00919 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -57,6 +57,7 @@ padding-bottom: 38px; } .openerp .oe_im_user { + position: relative; padding: 2px 6px; cursor: pointer; font-size: 13px; @@ -88,6 +89,17 @@ position: relative; } +.openerp .oe_im_user_online { + display: none; + position: absolute; + top: 9.5px; + right: 11px; + width: 11px; + height: 11px; + vertical-align: middle; + border: 0; +} + /* conversations */ .openerp .oe_im_chatview { @@ -106,6 +118,14 @@ } .openerp .oe_im_chatview .oe_im_chatview_disconnected { display:none; + position: absolute; + z-index: 100; + width: 100%; + background: #E8EBEF; + padding: 5px; + font-size: 11px; + color: #999; + line-height: 14px; } .openerp .oe_im_chatview .oe_im_chatview_header { padding: 3px 6px 2px; @@ -214,3 +234,10 @@ margin: 0 0 2px 30px; line-height: 14px; } + +.openerp .oe_im_chatview_online { + display: none; + margin-top: -4px; + width: 11px; + height: 11px; +} diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 9d6ebc4a4cb..076e900f2ae 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -90,7 +90,7 @@ openerp.web_im = function(instance) { search_changed: function(e) { var users = new instance.web.Model("res.users"); var self = this; - return this.user_search_dm.add(users.query(["name"]) + return this.user_search_dm.add(users.query(["name", "im_status"]) .filter([["name", "ilike", this.get("current_search")]]) .limit(USERS_LIMIT).all()).then(function(result) { self.add_to_user_cache(result); @@ -118,7 +118,7 @@ openerp.web_im = function(instance) { if (_.size(no_cache) === 0) return $.when(); else - return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name"]], + return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name", "im_status"]], {context: new instance.web.CompoundContext()}).then(function(users) { self.add_to_user_cache(users); }); @@ -201,6 +201,13 @@ openerp.web_im = function(instance) { this.user = user; this.user.add_watcher(); }, + start: function() { + var change_status = function() { + this.$(".oe_im_user_online").toggle(this.user.get("im_status") === true); + }; + this.user.on("change:im_status", this, change_status); + change_status.call(this); + }, activate_user: function() { this.trigger("activate_user", this.user); }, @@ -289,6 +296,13 @@ openerp.web_im = function(instance) { this.shown = true; }, start: function() { + var change_status = function() { + this.$(".oe_im_chatview_online").toggle(this.user.get("im_status") === true); + this.$(".oe_im_chatview_disconnected").toggle(this.user.get("im_status") === false); + }; + this.user.on("change:im_status", this, change_status); + change_status.call(this); + this.on("change:right_position", this, this.calc_pos); this.full_height = this.$el.height(); this.calc_pos(); diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 70551e3424b..33aaf6f18ed 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -24,11 +24,13 @@ +
+
From 329cf28f97295ec4e3094f6e9d96dfe91821ac8e Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 15:24:04 +0100 Subject: [PATCH 041/157] Made connect/disconnect work correctly and implemented mandatory optimisations bzr revid: nicolas.vanhoren@openerp.com-20121204142404-9wbior7y6sn1yt4b --- addons/web_im/im.py | 55 ++++++++++++++++++++----------- addons/web_im/static/src/js/im.js | 10 ++++++ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index e97556c7e40..1746c7c0b36 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -55,8 +55,10 @@ if openerp.tools.config.options["gevent"]: def __init__(self, db_name): self.db_name = db_name Watcher.watchers[db_name] = self - self.posted = gevent.event.Event() self.waiting = 0 + self.wait_id = 0 + self.users = {} + self.users_watch = {} gevent.spawn(self.loop) def loop(self): @@ -79,10 +81,13 @@ if openerp.tools.config.options["gevent"]: else: conn.poll() while conn.notifies: - notify = conn.notifies.pop().payload - # do something with it - self.posted.set() - self.posted.clear() + message = json.loads(conn.notifies.pop().payload) + if message["type"] == "message": + for waiter in self.users.get(message["receiver"], {}).values(): + waiter.set() + else: #type status + for waiter in self.users_watch.get(message["user"], {}).values(): + waiter.set() finally: try: c.execute("unlisten im_channel;") @@ -94,14 +99,26 @@ if openerp.tools.config.options["gevent"]: _logger.exception("Exception during instant messaging watcher activity") time.sleep(WATCHER_ERROR_DELAY) del Watcher.watchers[self.db_name] - self.posted.set() - self.posted = None _logger.info("End watching for instant messaging events for database " + self.db_name) - def stop(self, timeout=None): + def _get_wait_id(self): + self.wait_id += 1 + return self.wait_id + + def stop(self, user_id, watch_users, timeout=None): + wait_id = self._get_wait_id() + event = gevent.event.Event() self.waiting += 1 - self.posted.wait(timeout) - self.waiting -= 1 + self.users.setdefault(user_id, {})[wait_id] = event + for watch in watch_users: + self.users_watch.setdefault(watch, {})[wait_id] = event + try: + event.wait(timeout) + finally: + for watch in watch_users: + del self.users_watch[watch][wait_id] + del self.users[user_id][wait_id] + self.waiting -= 1 class ImportController(openerp.addons.web.http.Controller): @@ -119,7 +136,7 @@ class ImportController(openerp.addons.web.http.Controller): return res last = res["last"] num += 1 - Watcher.get_watcher(res["dbname"]).stop(POLL_TIMER) + Watcher.get_watcher(res["dbname"]).stop(req.session._uid, users_watch or [], POLL_TIMER) class im_message(osv.osv): @@ -177,18 +194,18 @@ class res_user(osv.osv): return res def im_connect(self, cr, uid, context=None): - self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, - "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) - cr.commit() - cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) - cr.commit() - return True + return self._im_change_status(cr, uid, True, context) def im_disconnect(self, cr, uid, context=None): - self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": True, + return self._im_change_status(cr, uid, False, context) + + def _im_change_status(self, cr, uid, new_one, context=None): + current_status = self.read(cr, openerp.SUPERUSER_ID, uid, ["im_status"], context=None)["im_status"] + self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": new_one, "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) cr.commit() - cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) + if current_status != new_one: + cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) cr.commit() return True diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 076e900f2ae..c47a51d3a05 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -53,6 +53,7 @@ openerp.web_im = function(instance) { }, this)); this.user_search_dm = new instance.web.DropMisordered(); this.users_cache = {}; + this.unload_event_handler = _.bind(this.unload, this); }, start: function() { this.$el.css("right", -this.$el.outerWidth()); @@ -65,6 +66,8 @@ openerp.web_im = function(instance) { var self = this; + $(window).on("unload", this.unload_event_handler); + return this.ensure_users([instance.session.uid]).then(function() { var me = self.users_cache[instance.session.uid]; delete self.users_cache[instance.session.uid]; @@ -77,6 +80,13 @@ openerp.web_im = function(instance) { }); }); }, + unload: function() { + return new instance.web.Model("res.users").call("im_disconnect", [], {context: new instance.web.CompoundContext()}); + }, + destroy: function() { + $(window).off("unload", this.unload_event_handler); + this._super(); + }, calc_box: function() { var $topbar = instance.client.$(".oe_topbar"); var top = $topbar.offset().top + $topbar.height(); From 61fc7a8dbee11468f2e3ba01af91acdebd64b5e5 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 15:27:45 +0100 Subject: [PATCH 042/157] Removed current user from search view bzr revid: nicolas.vanhoren@openerp.com-20121204142745-3md4p6r01634npl0 --- addons/web_im/static/src/js/im.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index c47a51d3a05..518b5634b84 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -102,6 +102,7 @@ openerp.web_im = function(instance) { var self = this; return this.user_search_dm.add(users.query(["name", "im_status"]) .filter([["name", "ilike", this.get("current_search")]]) + .filter([["id", "<>", instance.session.uid]]) .limit(USERS_LIMIT).all()).then(function(result) { self.add_to_user_cache(result); self.$(".oe_im_input").val(""); From 7ad062ca39a9e505db3b011713639969be86ba52 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 15:43:10 +0100 Subject: [PATCH 043/157] Polished disconnection style bzr revid: nicolas.vanhoren@openerp.com-20121204144310-a9l68frigqhmpysg --- addons/web_im/static/src/css/im.css | 9 ++++++++- addons/web_im/static/src/js/im.js | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index 51717c00919..b5053a9f757 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -118,7 +118,6 @@ } .openerp .oe_im_chatview .oe_im_chatview_disconnected { display:none; - position: absolute; z-index: 100; width: 100%; background: #E8EBEF; @@ -126,6 +125,11 @@ font-size: 11px; color: #999; line-height: 14px; + height: 28px; + overflow: hidden; +} +.openerp .oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected { + display: block; } .openerp .oe_im_chatview .oe_im_chatview_header { padding: 3px 6px 2px; @@ -156,6 +160,9 @@ overflow: auto; height: 287px; } +.openerp .oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content { + height: 249px; +} .openerp .oe_im_chatview .oe_im_chatview_footer { position: relative; padding: 3px; diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 518b5634b84..828c40e3c29 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -308,8 +308,9 @@ openerp.web_im = function(instance) { }, start: function() { var change_status = function() { + this.$el.toggleClass("oe_im_chatview_disconnected_status", this.user.get("im_status") === false); this.$(".oe_im_chatview_online").toggle(this.user.get("im_status") === true); - this.$(".oe_im_chatview_disconnected").toggle(this.user.get("im_status") === false); + this._go_bottom(); }; this.user.on("change:im_status", this, change_status); change_status.call(this); @@ -358,6 +359,9 @@ openerp.web_im = function(instance) { date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); this.last_bubble = $(QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date})); $(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble); + this._go_bottom(); + }, + _go_bottom: function() { this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); }, destroy: function() { From 7d52b3dbbf1314aabb3b25f8212160f30b909bd6 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 15:57:56 +0100 Subject: [PATCH 044/157] Some minor improvements bzr revid: nicolas.vanhoren@openerp.com-20121204145756-pxzhvjjmvb21yegw --- addons/web_im/im.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 1746c7c0b36..1e365daaae9 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -149,7 +149,7 @@ class im_message(osv.osv): } _defaults = { - 'date': datetime.datetime.now(), + 'date': datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT), } def get_messages(self, cr, uid, last=None, users_watch=None, context=None): @@ -188,9 +188,10 @@ class res_user(osv.osv): res = {} current = datetime.datetime.now() delta = datetime.timedelta(0, DISCONNECTION_TIMER) - for obj in self.browse(cr, uid, ids, context=context): - last_update = datetime.datetime.strptime(obj.im_last_status_update, DEFAULT_SERVER_DATETIME_FORMAT) - res[obj.id] = obj.im_last_status and (last_update + delta) > current + data = self.read(cr, uid, ids, ["im_last_status_update", "im_last_status"], context=context) + for obj in data: + last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT) + res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current return res def im_connect(self, cr, uid, context=None): From 17b0be4505b35bdf68c800da117fe85ce6e3db1a Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 16:58:17 +0100 Subject: [PATCH 045/157] Solved problem with serializable on res.users bzr revid: nicolas.vanhoren@openerp.com-20121204155817-bvxcp8hzy6ks66n4 --- addons/web_im/im.py | 6 +++--- addons/web_im/static/src/js/im.js | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 1e365daaae9..4d5c34eb041 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -128,7 +128,7 @@ class ImportController(openerp.addons.web.http.Controller): def poll(self, req, last=None, users_watch=None): if not openerp.tools.config.options["gevent"]: raise Exception("Not usable in a server not running gevent") - res = req.session.model('res.users').im_connect(context=req.context) + req.session.model('res.users').im_connect(context=req.context) num = 0 while True: res = req.session.model('im.message').get_messages(last, users_watch, req.context) @@ -144,7 +144,7 @@ class im_message(osv.osv): _columns = { 'message': fields.char(string="Message", size=200, required=True), 'from': fields.many2one("res.users", "From", required= True, ondelete='cascade'), - 'to': fields.many2one("res.users", "From", required=True, select=True, ondelete='cascade'), + 'to': fields.many2one("res.users", "To", required=True, select=True, ondelete='cascade'), 'date': fields.datetime("Date", required=True), } @@ -207,7 +207,7 @@ class res_user(osv.osv): cr.commit() if current_status != new_one: cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})]) - cr.commit() + cr.commit() return True _columns = { diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 828c40e3c29..49fa9012f31 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -343,9 +343,20 @@ openerp.web_im = function(instance) { } var mes = this.$("input").val(); this.$("input").val(""); - this._add_bubble(this.me, mes, instance.web.datetime_to_str(new Date())); - var model = new instance.web.Model("im.message"); - model.call("post", [mes, this.user.get('id')], {context: new instance.web.CompoundContext()}); + var send_it = _.bind(function() { + var model = new instance.web.Model("im.message"); + return model.call("post", [mes, this.user.get('id')], + {context: new instance.web.CompoundContext()}); + }, this); + var tries = 0; + send_it().then(_.bind(function() { + this._add_bubble(this.me, mes, instance.web.datetime_to_str(new Date())); + }, this), function(error, e) { + e.preventDefault(); + tries += 1; + if (tries < 3) + return send_it(); + }); }, _add_bubble: function(user, item, date) { var items = [item]; From 8e1eb820f1392cd105af26f633f263b631d0e855 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 17:43:30 +0100 Subject: [PATCH 046/157] Changed dates bzr revid: nicolas.vanhoren@openerp.com-20121204164330-1h2dr0wcc2txouo3 --- addons/web_im/static/src/css/im.css | 9 ++++++++- addons/web_im/static/src/js/im.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index b5053a9f757..5955aa414c9 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -190,6 +190,7 @@ } .openerp .oe_im_chatview .oe_im_chatview_bubble { background: white; + position: relative; padding: 3px; margin: 3px; -moz-border-radius: 3px; @@ -219,17 +220,23 @@ -ms-interpolation-mode: bicubic; } .openerp .oe_im_chatview .oe_im_chatview_time { + position: absolute; + right: 0px; + top: 0px; + margin: 3px; text-align: right; line-height: 13px; font-size: 11px; color: #999; + width: 60px; + overflow: hidden; } .openerp .oe_im_chatview .oe_im_chatview_from { margin: 0 0 2px 0; line-height: 14px; font-weight: bold; font-size: 12px; - width: 154px; + width: 140px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 49fa9012f31..d68edd7aeaf 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -367,7 +367,14 @@ openerp.web_im = function(instance) { this.last_user = user; this.last_items = items; date = instance.web.str_to_datetime(date); - date = date.toString(Date.CultureInfo.formatPatterns.shortDate + " " + Date.CultureInfo.formatPatterns.shortTime); + var now = new Date(); + var diff = now - date; + if (diff > (1000 * 60 * 60 * 24)) { + date = $.timeago(date); + } else { + date = date.toString(Date.CultureInfo.formatPatterns.shortTime); + } + this.last_bubble = $(QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date})); $(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble); this._go_bottom(); From 8b00cbda38655656b07a93957c3a4101b38c6ae0 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 4 Dec 2012 17:48:11 +0100 Subject: [PATCH 047/157] Reduced opacity bzr revid: nicolas.vanhoren@openerp.com-20121204164811-2as5ebkr1z7v1c70 --- addons/web_im/static/src/css/im.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_im/static/src/css/im.css b/addons/web_im/static/src/css/im.css index 5955aa414c9..0ab7147fcdd 100644 --- a/addons/web_im/static/src/css/im.css +++ b/addons/web_im/static/src/css/im.css @@ -107,7 +107,7 @@ overflow: hidden; bottom: 6px; margin-right: 6px; - background: rgba(0, 0, 0, 0.6); + background: rgba(60, 60, 60, 0.8); -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; From e96796df038cff54d83ad759502497b203f530a2 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 6 Dec 2012 17:39:07 +0100 Subject: [PATCH 048/157] Modified the place where status information are stored to avoid openerp crash because it doesn't support simultaneous access to the same row. bzr revid: nicolas.vanhoren@openerp.com-20121206163907-ncegsrn77wmrl5t0 --- addons/web_im/im.py | 60 ++++++++++++++++++---- addons/web_im/security/ir.model.access.csv | 3 +- addons/web_im/static/src/js/im.js | 13 +++-- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 4d5c34eb041..5c1d9d53b54 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -128,7 +128,7 @@ class ImportController(openerp.addons.web.http.Controller): def poll(self, req, last=None, users_watch=None): if not openerp.tools.config.options["gevent"]: raise Exception("Not usable in a server not running gevent") - req.session.model('res.users').im_connect(context=req.context) + req.session.model('im.user').im_connect(context=req.context) num = 0 while True: res = req.session.model('im.message').get_messages(last, users_watch, req.context) @@ -156,11 +156,12 @@ class im_message(osv.osv): users_watch = users_watch or [] # complex stuff to determine the last message to show - users = self.pool.get("res.users") - c_user = users.browse(cr, uid, uid, context=context) + users = self.pool.get("im.user") + my_id = users.get_by_user_ids(cr, uid, [uid], context=context)[0]["id"] + c_user = users.browse(cr, uid, my_id, context=context) if last: if c_user.im_last_received < last: - users.write(cr, openerp.SUPERUSER_ID, uid, {'im_last_received': last}, context=context) + users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context) else: last = c_user.im_last_received or -1 @@ -168,7 +169,11 @@ class im_message(osv.osv): res = self.read(cr, uid, res, ["id", "message", "from", "date"], context=context) if len(res) > 0: last = res[-1]["id"] - users_status = users.read(cr, uid, users_watch, ["im_status"], context=context) + users_watch = users.get_by_user_ids(cr, uid, users_watch, context=context) + users_status = users.read(cr, uid, [x["id"] for x in users_watch], ["im_status", "user"], context=context) + for x in users_status: + x["id"] = x["user"][0] + del x["user"] return {"res": res, "last": last, "dbname": cr.dbname, "users_status": users_status} def post(self, cr, uid, message, to_user_id, context=None): @@ -181,8 +186,8 @@ class im_message(osv.osv): def activated(self, cr, uid, context=None): return not not openerp.tools.config.options["gevent"] -class res_user(osv.osv): - _inherit = "res.users" +class im_user(osv.osv): + _name = "im.user" def _im_status(self, cr, uid, ids, something, something_else, context=None): res = {} @@ -194,6 +199,24 @@ class res_user(osv.osv): res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current return res + def search_users(self, cr, uid, domain, fields, limit, context=None): + found = self.pool.get('res.users').search(cr, uid, domain, limit=limit, context=context) + return self.read_users(cr, uid, found, fields, context) + + def read_users(self, cr, uid, ids, fields, context=None): + users = self.pool.get('res.users').read(cr, uid, ids, fields, context=context) + statuses = self.get_by_user_ids(cr, uid, ids, context=context) + statuses = self.read(cr, uid, [x["id"] for x in statuses], context = context) + by_id = {} + for x in statuses: + by_id[x["user"][0]] = x + res = [] + for x in users: + d = by_id[x["id"]] + d.update(x) + res.append(d) + return res + def im_connect(self, cr, uid, context=None): return self._im_change_status(cr, uid, True, context) @@ -201,8 +224,10 @@ class res_user(osv.osv): return self._im_change_status(cr, uid, False, context) def _im_change_status(self, cr, uid, new_one, context=None): - current_status = self.read(cr, openerp.SUPERUSER_ID, uid, ["im_status"], context=None)["im_status"] - self.write(cr, openerp.SUPERUSER_ID, uid, {"im_last_status": new_one, + ids = self.get_by_user_ids(cr, uid, [uid], context=context) + id = ids[0]["id"] + current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"] + self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one, "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) cr.commit() if current_status != new_one: @@ -210,7 +235,24 @@ class res_user(osv.osv): cr.commit() return True + def get_by_user_ids(self, cr, uid, ids, context=None): + users = self.search(cr, uid, [["user", "in", ids]], context=None) + records = self.read(cr, openerp.SUPERUSER_ID, users, ["user"], context=None) + inside = {} + for i in records: + inside[i["user"][0]] = True + not_inside = {} + for i in ids: + if not (i in inside): + not_inside[i] = True + for to_create in not_inside.keys(): + created = self.create(cr, openerp.SUPERUSER_ID, {"user": to_create}, context=context) + records.append({"id": created, "user": [to_create, ""]}) + return records + + _columns = { + 'user': fields.many2one("res.users", string="User", select=True), 'im_last_received': fields.integer(string="Instant Messaging Last Received Message"), 'im_last_status': fields.boolean(strint="Instant Messaging Last Status"), 'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"), diff --git a/addons/web_im/security/ir.model.access.csv b/addons/web_im/security/ir.model.access.csv index 4350b6a5941..1b6fa51262a 100644 --- a/addons/web_im/security/ir.model.access.csv +++ b/addons/web_im/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_im_message,im.message,model_im_message,,1,1,1,1 \ No newline at end of file +access_im_message,im.message,model_im_message,,1,1,1,1 +access_im_user,im.user,model_im_user,,1,1,1,1 \ No newline at end of file diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index d68edd7aeaf..d3274868e27 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -81,7 +81,7 @@ openerp.web_im = function(instance) { }); }, unload: function() { - return new instance.web.Model("res.users").call("im_disconnect", [], {context: new instance.web.CompoundContext()}); + return new instance.web.Model("im.user").call("im_disconnect", [], {context: new instance.web.CompoundContext()}); }, destroy: function() { $(window).off("unload", this.unload_event_handler); @@ -98,12 +98,11 @@ openerp.web_im = function(instance) { this.set("current_search", this.$(".oe_im_searchbox").val()); }, search_changed: function(e) { - var users = new instance.web.Model("res.users"); + var users = new instance.web.Model("im.user"); var self = this; - return this.user_search_dm.add(users.query(["name", "im_status"]) - .filter([["name", "ilike", this.get("current_search")]]) - .filter([["id", "<>", instance.session.uid]]) - .limit(USERS_LIMIT).all()).then(function(result) { + return this.user_search_dm.add(users.call("search_users", + [[["name", "ilike", this.get("current_search")], ["id", "<>", instance.session.uid]], + ["name"], USERS_LIMIT], {context:new instance.web.CompoundContext()})).then(function(result) { self.add_to_user_cache(result); self.$(".oe_im_input").val(""); var old_users = self.users; @@ -129,7 +128,7 @@ openerp.web_im = function(instance) { if (_.size(no_cache) === 0) return $.when(); else - return new instance.web.Model("res.users").call("read", [_.values(no_cache), ["name", "im_status"]], + return new instance.web.Model("im.user").call("read_users", [_.values(no_cache), ["name"]], {context: new instance.web.CompoundContext()}).then(function(users) { self.add_to_user_cache(users); }); From 1211ee2ae57c4b6dfa72afbd44733b20a2675983 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 10 Jan 2013 16:20:26 +0100 Subject: [PATCH 049/157] [FIX] problem when trying to launch 2 servers, one for normal requests and one for im bzr revid: nicolas.vanhoren@openerp.com-20130110152026-21nf54bc79q4xw9q --- addons/web_im/im.py | 7 ++++--- addons/web_im/static/src/js/im.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 5c1d9d53b54..0fd03f4c085 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -138,6 +138,10 @@ class ImportController(openerp.addons.web.http.Controller): num += 1 Watcher.get_watcher(res["dbname"]).stop(req.session._uid, users_watch or [], POLL_TIMER) + @openerp.addons.web.http.jsonrequest + def activated(self, req): + return not not openerp.tools.config.options["gevent"] + class im_message(osv.osv): _name = 'im.message' @@ -183,9 +187,6 @@ class im_message(osv.osv): cr.commit() return False - def activated(self, cr, uid, context=None): - return not not openerp.tools.config.options["gevent"] - class im_user(osv.osv): _name = "im.user" diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index d3274868e27..1c0896468ba 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -72,7 +72,7 @@ openerp.web_im = function(instance) { var me = self.users_cache[instance.session.uid]; delete self.users_cache[instance.session.uid]; self.c_manager.set_me(me); - return new instance.web.Model("im.message").call("activated", [], {context: new instance.web.CompoundContext()}).then(function(activated) { + self.rpc("/im/activated", {}).then(function(activated) { if (activated) { self.activated = true; self.poll(); From a9d4f087e70d7031880e0a6d7983822b7be462b4 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 10:57:56 +0100 Subject: [PATCH 050/157] Changed long polling address bzr revid: nicolas.vanhoren@openerp.com-20130111095756-8p16hkb0v9jyc5hd --- addons/web_im/im.py | 2 +- addons/web_im/static/src/js/im.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web_im/im.py b/addons/web_im/im.py index 0fd03f4c085..c080e522315 100644 --- a/addons/web_im/im.py +++ b/addons/web_im/im.py @@ -122,7 +122,7 @@ if openerp.tools.config.options["gevent"]: class ImportController(openerp.addons.web.http.Controller): - _cp_path = '/im' + _cp_path = '/longpolling/im' @openerp.addons.web.http.jsonrequest def poll(self, req, last=None, users_watch=None): diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 1c0896468ba..617811b855b 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -72,7 +72,7 @@ openerp.web_im = function(instance) { var me = self.users_cache[instance.session.uid]; delete self.users_cache[instance.session.uid]; self.c_manager.set_me(me); - self.rpc("/im/activated", {}).then(function(activated) { + self.rpc("/longpolling/im/activated", {}).then(function(activated) { if (activated) { self.activated = true; self.poll(); @@ -174,7 +174,7 @@ openerp.web_im = function(instance) { var user_ids = _.map(this.users_cache, function(el) { return el.get("id"); }); - this.rpc("/im/poll", { + this.rpc("/longpolling/im/poll", { last: this.last, users_watch: user_ids, context: instance.web.pyeval.eval('context', {}), From ae546a87651ddadb5c9e5338c984c46f39028b5c Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 11:34:59 +0100 Subject: [PATCH 051/157] Added audio bzr revid: nicolas.vanhoren@openerp.com-20130111103459-3912bcim92yvzwxe --- addons/web_im/static/src/audio/Ting.mp3 | Bin 0 -> 17087 bytes addons/web_im/static/src/audio/Ting.ogg | Bin 0 -> 12568 bytes addons/web_im/static/src/js/im.js | 1 + addons/web_im/static/src/xml/im.xml | 4 ++++ 4 files changed, 5 insertions(+) create mode 100644 addons/web_im/static/src/audio/Ting.mp3 create mode 100644 addons/web_im/static/src/audio/Ting.ogg diff --git a/addons/web_im/static/src/audio/Ting.mp3 b/addons/web_im/static/src/audio/Ting.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6fd090a89ce6c3391a36b3f1fb94d3679a0dd7fa GIT binary patch literal 17087 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 literal 0 HcmV?d00001 diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 617811b855b..5ee36b51547 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -335,6 +335,7 @@ openerp.web_im = function(instance) { }, received_message: function(message) { this._add_bubble(this.user, message.message, message.date); + this.$("audio")[0].play(); }, send_message: function(e) { if(e && e.which !== 13) { diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 33aaf6f18ed..584eaadd8e2 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -43,6 +43,10 @@ +
From 730df4c79225652e0365a4c59f505978ed09c24c Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 14:44:17 +0100 Subject: [PATCH 052/157] Now changes the window title when we have a new message and the current tab is not active. bzr revid: nicolas.vanhoren@openerp.com-20130111134417-a93cocejp4tiuy8b --- addons/web_im/static/src/js/im.js | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index 5ee36b51547..b4f1415b1dd 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -257,6 +257,31 @@ openerp.web_im = function(instance) { this.conversations = []; this.users = {}; this.on("change:right_offset", this, this.calc_positions); + this.set("window_focus", true); + this.set("waiting_messages", 0); + this.focus_hdl = _.bind(function() { + this.set("window_focus", true); + }, this); + $(window).bind("focus", this.focus_hdl); + this.blur_hdl = _.bind(function() { + this.set("window_focus", false); + }, this); + $(window).bind("blur", this.blur_hdl); + this.on("change:window_focus", this, this.window_focus_change); + this.window_focus_change(); + this.on("change:waiting_messages", this, this.messages_change); + this.messages_change(); + }, + window_focus_change: function() { + if (this.get("window_focus")) { + this.set("waiting_messages", 0); + } + }, + messages_change: function() { + if (! instance.webclient.set_title_part) + return; + instance.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined : + _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))); }, set_me: function(me) { this.me = me; @@ -278,6 +303,9 @@ openerp.web_im = function(instance) { return conv; }, received_message: function(message, user) { + if (! this.get("window_focus")) { + this.set("waiting_messages", this.get("waiting_messages") + 1); + } var conv = this.activate_user(user); conv.received_message(message); }, @@ -288,6 +316,11 @@ openerp.web_im = function(instance) { current += this.conversations[i].$el.outerWidth(true); }, this); }, + destroy: function() { + $(window).unbind("blur", this.blur_hdl); + $(window).unbind("focus", this.focus_hdl); + this._super(); + }, }); instance.web_im.Conversation = instance.web.Widget.extend({ From 90164bfc1a519cf34dec7aeb6744ca7df83cd145 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 14:58:17 +0100 Subject: [PATCH 053/157] Play the "tink" only when the window is not active bzr revid: nicolas.vanhoren@openerp.com-20130111135817-d6orh4d4t851nra1 --- addons/web_im/static/src/js/im.js | 5 ++++- addons/web_im/static/src/xml/im.xml | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index b4f1415b1dd..c8bf0c21ff8 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -271,6 +271,8 @@ openerp.web_im = function(instance) { this.window_focus_change(); this.on("change:waiting_messages", this, this.messages_change); this.messages_change(); + this.$ting = $(QWeb.render("Conversation.ting")); + this.$ting.appendTo($("body")); }, window_focus_change: function() { if (this.get("window_focus")) { @@ -305,6 +307,7 @@ openerp.web_im = function(instance) { received_message: function(message, user) { if (! this.get("window_focus")) { this.set("waiting_messages", this.get("waiting_messages") + 1); + this.$ting[0].play(); } var conv = this.activate_user(user); conv.received_message(message); @@ -317,6 +320,7 @@ openerp.web_im = function(instance) { }, this); }, destroy: function() { + this.$ting.remove(); $(window).unbind("blur", this.blur_hdl); $(window).unbind("focus", this.focus_hdl); this._super(); @@ -368,7 +372,6 @@ openerp.web_im = function(instance) { }, received_message: function(message) { this._add_bubble(this.user, message.message, message.date); - this.$("audio")[0].play(); }, send_message: function(e) { if(e && e.which !== 13) { diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 584eaadd8e2..2031d514365 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -43,10 +43,6 @@ -
@@ -63,4 +59,10 @@
+ + + \ No newline at end of file From 88fe79c7c0ebfc716ffd1110d4ae1fd4f0bbc698 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 16:04:26 +0100 Subject: [PATCH 054/157] Fixed bug in chrome: can not play the sound multiple times bzr revid: nicolas.vanhoren@openerp.com-20130111150426-4uk858n12lf2a3gm --- addons/web_im/static/src/js/im.js | 14 ++++++++++---- addons/web_im/static/src/xml/im.xml | 6 ------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/addons/web_im/static/src/js/im.js b/addons/web_im/static/src/js/im.js index c8bf0c21ff8..718fd8aaeac 100644 --- a/addons/web_im/static/src/js/im.js +++ b/addons/web_im/static/src/js/im.js @@ -271,8 +271,14 @@ openerp.web_im = function(instance) { this.window_focus_change(); this.on("change:waiting_messages", this, this.messages_change); this.messages_change(); - this.$ting = $(QWeb.render("Conversation.ting")); - this.$ting.appendTo($("body")); + this.create_ting(); + }, + create_ting: function() { + if (new Audio().canPlayType("audio/ogg; codecs=vorbis")) { + this.ting = new Audio(instance.webclient.session.origin + "/web_im/static/src/audio/Ting.ogg"); + } else { + this.ting = new Audio(instance.webclient.session.origin + "/web_im/static/src/audio/Ting.mp3"); + } }, window_focus_change: function() { if (this.get("window_focus")) { @@ -307,7 +313,8 @@ openerp.web_im = function(instance) { received_message: function(message, user) { if (! this.get("window_focus")) { this.set("waiting_messages", this.get("waiting_messages") + 1); - this.$ting[0].play(); + this.ting.play(); + this.create_ting(); } var conv = this.activate_user(user); conv.received_message(message); @@ -320,7 +327,6 @@ openerp.web_im = function(instance) { }, this); }, destroy: function() { - this.$ting.remove(); $(window).unbind("blur", this.blur_hdl); $(window).unbind("focus", this.focus_hdl); this._super(); diff --git a/addons/web_im/static/src/xml/im.xml b/addons/web_im/static/src/xml/im.xml index 2031d514365..33aaf6f18ed 100644 --- a/addons/web_im/static/src/xml/im.xml +++ b/addons/web_im/static/src/xml/im.xml @@ -59,10 +59,4 @@
- - - \ No newline at end of file From fe2dfadc81a76b67c027a24c5455a3d762f970ab Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 11 Jan 2013 16:21:29 +0100 Subject: [PATCH 055/157] Made sound notifications perfect bzr revid: nicolas.vanhoren@openerp.com-20130111152129-eb5mwy5cuamc219e --- addons/web_im/static/src/audio/purr.mp3 | Bin 0 -> 85262 bytes addons/web_im/static/src/audio/purr.ogg | Bin 0 -> 47390 bytes addons/web_im/static/src/js/im.js | 8 +++----- 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 addons/web_im/static/src/audio/purr.mp3 create mode 100644 addons/web_im/static/src/audio/purr.ogg diff --git a/addons/web_im/static/src/audio/purr.mp3 b/addons/web_im/static/src/audio/purr.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..849d303c3b1dab9ae688e53dc3ecdcf844e4d484 GIT binary patch literal 85262 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 f483WX@rs-8%*Nuw!UyMZPBylr%*avQR1E~HU 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 Date: Wed, 16 Jan 2013 14:43:48 +0100 Subject: [PATCH 056/157] Initial setup + import of requirejs tests bzr revid: nicolas.vanhoren@openerp.com-20130116134348-4dt6xv2jxfh4fcsd --- addons/live_support/__init__.py | 2 + addons/live_support/__openerp__.py | 21 + addons/live_support/live_support.py | 20 + .../live_support/security/ir.model.access.csv | 1 + .../static/ext/static/js/jquery.js | 9555 +++++++++++++++++ .../static/ext/static/js/livesupport.js | 8 + .../live_support/static/ext/static/js/nova.js | 981 ++ .../static/ext/static/js/require.js | 1993 ++++ .../static/ext/static/js/underscore.js | 1221 +++ addons/live_support/static/ext/tmp.html | 22 + addons/live_support/static/src/css/im.css | 0 addons/live_support/static/src/js/im.js | 8 + addons/live_support/static/src/xml/im.xml | 5 + 13 files changed, 13837 insertions(+) create mode 100644 addons/live_support/__init__.py create mode 100644 addons/live_support/__openerp__.py create mode 100644 addons/live_support/live_support.py create mode 100644 addons/live_support/security/ir.model.access.csv create mode 100644 addons/live_support/static/ext/static/js/jquery.js create mode 100644 addons/live_support/static/ext/static/js/livesupport.js create mode 100644 addons/live_support/static/ext/static/js/nova.js create mode 100644 addons/live_support/static/ext/static/js/require.js create mode 100644 addons/live_support/static/ext/static/js/underscore.js create mode 100644 addons/live_support/static/ext/tmp.html create mode 100644 addons/live_support/static/src/css/im.css create mode 100644 addons/live_support/static/src/js/im.js create mode 100644 addons/live_support/static/src/xml/im.xml diff --git a/addons/live_support/__init__.py b/addons/live_support/__init__.py new file mode 100644 index 00000000000..dfae1c168f5 --- /dev/null +++ b/addons/live_support/__init__.py @@ -0,0 +1,2 @@ + +import live_support diff --git a/addons/live_support/__openerp__.py b/addons/live_support/__openerp__.py new file mode 100644 index 00000000000..506a9ac8dfc --- /dev/null +++ b/addons/live_support/__openerp__.py @@ -0,0 +1,21 @@ +{ + 'name' : 'Live Support', + 'version': '1.0', + 'category': 'Tools', + 'complexity': 'easy', + 'description': + """ +OpenERP Live Support +==================== +Allow to drop instant messaging widgets on any web page that will communicate with the current +server. + """, + 'data': [ + ], + 'depends' : [], + 'js': ['static/src/js/*.js'], + 'css': ['static/src/css/*.css'], + 'qweb': ['static/src/xml/*.xml'], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py new file mode 100644 index 00000000000..45de899ed49 --- /dev/null +++ b/addons/live_support/live_support.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## diff --git a/addons/live_support/security/ir.model.access.csv b/addons/live_support/security/ir.model.access.csv new file mode 100644 index 00000000000..97dd8b917b8 --- /dev/null +++ b/addons/live_support/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/addons/live_support/static/ext/static/js/jquery.js b/addons/live_support/static/ext/static/js/jquery.js new file mode 100644 index 00000000000..ded03845983 --- /dev/null +++ b/addons/live_support/static/ext/static/js/jquery.js @@ -0,0 +1,9555 @@ +/*! + * jQuery JavaScript Library v1.9.0 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2013-1-14 + */ +(function( window, undefined ) { +"use strict"; +var + // A central reference to the root jQuery(document) + rootjQuery, + + // The deferred used on DOM ready + readyList, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + location = window.location, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // [[Class]] -> type pairs + class2type = {}, + + // List of deleted data cache ids, so we can reuse them + core_deletedIds = [], + + core_version = "1.9.0", + + // Save a reference to some core methods + core_concat = core_deletedIds.concat, + core_push = core_deletedIds.push, + core_slice = core_deletedIds.slice, + core_indexOf = core_deletedIds.indexOf, + core_toString = class2type.toString, + core_hasOwn = class2type.hasOwnProperty, + core_trim = core_version.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, + + // Used for splitting on whitespace + core_rnotwhite = /\S+/g, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + + // The ready event handler and self cleanup method + DOMContentLoaded = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + } else if ( document.readyState === "complete" ) { + // we're here because readyState === "complete" in oldIE + // which is good enough for us to call the dom ready! + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: core_version, + + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + if ( obj == null ) { + return String( obj ); + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ core_toString.call(obj) ] || "object" : + typeof obj; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; + } + context = context || document; + + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } + return jQuery.merge( [], parsed.childNodes ); + }, + + parseJSON: function( data ) { + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + if ( data === null ) { + return data; + } + + if ( typeof data === "string" ) { + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } + + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + core_push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return core_concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + } +}); + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + return jQuery.inArray( fn, list ) > -1; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function() { + + var support, all, a, select, opt, input, fragment, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
a"; + + // Support tests won't run in some limited or non-browser environments + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + if ( !all || !a || !all.length ) { + return {}; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + support = { + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.5/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + checkOn: !!input.value, + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Tests for enctype support on a form (#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: document.compatMode === "CSS1Compat", + + // Will be defined later + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
t
"; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
"; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + body.style.zoom = 1; + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})(); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt /* For internal use only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data, false ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name, false ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + // Try to fetch any internally stored data first + return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + } + + this.each(function() { + jQuery.data( this, key, value ); + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + hooks.cur = fn; + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + // Toggle whole class name + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val, + self = jQuery(this); + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + // In IE9+, Flash objects don't have .getAttribute (#12945) + // Support: IE9+ + if ( typeof elem.getAttribute !== "undefined" ) { + ret = elem.getAttribute( name ); + } + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( rboolean.test( name ) ) { + // Set corresponding property to false for boolean attributes + // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 + if ( !getSetAttribute && ruseDefault.test( name ) ) { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } else { + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + var + // Use .prop to determine if this attribute is understood as boolean + prop = jQuery.prop( elem, name ), + + // Fetch it accordingly + attr = typeof prop === "boolean" && elem.getAttribute( name ), + detail = typeof prop === "boolean" ? + + getSetInput && getSetAttribute ? + attr != null : + // oldIE fabricates an empty string for missing boolean attributes + // and conflates checked/selected into attroperties + ruseDefault.test( name ) ? + elem[ jQuery.camelCase( "default-" + name ) ] : + !!attr : + + // fetch an attribute node for properties not recognized as boolean + elem.getAttributeNode( name ); + + return detail && detail.value !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; + +// fix oldIE value attroperty +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return jQuery.nodeName( elem, "input" ) ? + + // Ignore the value *property* by using defaultValue + elem.defaultValue : + + ret && ret.specified ? ret.value : undefined; + }, + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret == null ? undefined : ret; + } + }); + }); + + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + // Don't attach events to noData or text/comment nodes (but allow plain objects) + elemData = elem.nodeType !== 3 && elem.nodeType !== 8 && jQuery._data( elem ); + + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = event.type || event, + namespaces = event.namespace ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + event.isTrigger = true; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur != this; cur = cur.parentNode || this ) { + + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + } + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== document.activeElement && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === document.activeElement && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + + beforeunload: { + postDispatch: function( event ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === "undefined" ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { + +var i, + cachedruns, + Expr, + getText, + isXML, + compile, + hasDuplicate, + outermostContext, + + // Local document vars + setDocument, + document, + docElem, + documentIsXML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + sortOrder, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + support = {}, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Array methods + arr = [], + pop = arr.pop, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rsibling = /[\x20\t\r\n\f]*[+~]/, + + rnative = /\{\s*\[native code\]\s*\}/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, + funescape = function( _, escaped ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + return high !== high ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Use a stripped-down slice if we can't use a native one +try { + slice.call( docElem.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + for ( ; (elem = this[i]); i++ ) { + results.push( elem ); + } + return results; + }; +} + +/** + * For feature detection + * @param {Function} fn The function to test for native support + */ +function isNative( fn ) { + return rnative.test( fn + "" ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var cache, + keys = []; + + return (cache = function( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); + }); +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; + } +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( !documentIsXML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + + // QSA path + if ( support.qsa && !rbuggyQSA.test(selector) ) { + old = true; + nid = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsXML = isXML( doc ); + + // Check if getElementsByTagName("*") returns only elements + support.tagNameNoComments = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if attributes should be retrieved by attribute nodes + support.attributes = assert(function( div ) { + div.innerHTML = ""; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }); + + // Check if getElementsByClassName can be trusted + support.getByClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = ""; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }); + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + support.getByName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "
"; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = doc.getElementsByName && + // buggy browsers will return fewer than the correct 2 + doc.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + doc.getElementsByName( expando + 0 ).length; + support.getIdNotName = !doc.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + + // IE6/7 return modified attributes + Expr.attrHandle = assert(function( div ) { + div.innerHTML = ""; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }) ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }; + + // ID find and filter + if ( support.getIdNotName ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.tagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Name + Expr.find["NAME"] = support.getByName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }; + + // Class + Expr.find["CLASS"] = support.getByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { + return context.getElementsByClassName( className ); + } + }; + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21), + // no need to also add to buggyMatches since matches checks buggyQSA + // A support test would require too much code (would include document ready) + rbuggyQSA = [ ":focus" ]; + + if ( (support.qsa = isNative(doc.querySelectorAll)) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE8 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = ""; + if ( div.querySelectorAll("[i^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + // Document order sorting + sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + var compare; + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { + if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { + if ( a === doc || contains( preferredDoc, a ) ) { + return -1; + } + if ( b === doc || contains( preferredDoc, b ) ) { + return 1; + } + return 0; + } + return compare & 4 ? -1 : 1; + } + + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return ( ~b.sourceIndex || MAX_NEGATIVE ) - ( contains( preferredDoc, a ) && ~a.sourceIndex || MAX_NEGATIVE ); + + // Parentless nodes are either documents or disconnected + } else if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + // Always assume the presence of duplicates if sort doesn't + // pass them to our comparison function (as in Google Chrome). + hasDuplicate = false; + [0, 0].sort( sortOrder ); + support.detectDuplicates = hasDuplicate; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyQSA always contains :focus, so no need for an existence check + if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [elem] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + var val; + + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + if ( !documentIsXML ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( documentIsXML || support.attributes ) { + return elem.getAttribute( name ); + } + return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? + name : + val && val.specified ? val.value : null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + i = 1, + j = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + return results; +}; + +function siblingCheck( a, b ) { + var cur = a && b && a.nextSibling; + + for ( ; cur; cur = cur.nextSibling ) { + if ( cur === b ) { + return -1; + } + } + + return a ? 1 : -1; +} + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[5] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[4] ) { + match[2] = match[4]; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + + nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.substr( result.length - check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifider + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsXML ? + elem.getAttribute("xml:lang") || elem.getAttribute("lang") : + elem.lang) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push( { + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && combinator.dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var data, cache, outerCache, + dirkey = dirruns + " " + doneName; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Nested matchers should use non-integer dirruns + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } + + // Add elements passing elementMatchers directly to results + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + for ( j = 0; (matcher = elementMatchers[j]); j++ ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++matcherCachedRuns; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + // `i` starts as a string, so matchedCount would equal "00" if there are no elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + for ( j = 0; (matcher = setMatchers[j]); j++ ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !documentIsXML && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + for ( i = matchExpr["needsContext"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && context.parentNode || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + documentIsXML, + results, + rsibling.test( selector ) + ); + return results; +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Easy API for creating new setFilters +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Initialize with the default document +setDocument(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, ret, self; + + if ( typeof selector !== "string" ) { + self = this; + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < self.length; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + ret = []; + for ( i = 0; i < this.length; i++ ) { + jQuery.find( selector, this[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( jQuery.unique( ret ) ); + ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true) ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + cur = this[i]; + + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + } + cur = cur.parentNode; + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +jQuery.fn.andSelf = jQuery.fn.addBack; + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( this.length > 1 && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + col: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
", "
" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function( value ) { + var isFunc = jQuery.isFunction( value ); + + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( !isFunc && typeof value !== "string" ) { + value = jQuery( value ).not( this ).detach(); + } + + return this.domManip( [ value ], true, function( elem ) { + var next = this.nextSibling, + parent = this.parentNode; + + if ( parent && this.nodeType === 1 || this.nodeType === 11 ) { + + jQuery( this ).remove(); + + if ( next ) { + next.parentNode.insertBefore( elem, next ); + } else { + parent.appendChild( elem ); + } + } + }); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, table ? self.html() : undefined ); + } + self.domManip( args, table, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : + this[i], + node, + i + ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery.ajax({ + url: node.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + var attr = elem.getAttributeNode("type"); + elem.type = ( attr && attr.specified ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, data, e; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, srcElements, node, i, clone, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var contains, elem, tag, tmp, wrap, tbody, j, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
" && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var data, id, elem, type, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== "undefined" ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + } +}); +var curCSS, getStyles, iframe, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var elem, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + values[ index ] = jQuery._data( elem, "olddisplay" ); + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && elem.style.display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else if ( !values[ index ] && !isHidden( elem ) ) { + jQuery._data( elem, "olddisplay", jQuery.css( elem, "display" ) ); + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + var bool = typeof state === "boolean"; + + return this.each(function() { + if ( bool ? state : isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + // Use the already-created iframe if possible + iframe = ( iframe || + jQuery(" \ No newline at end of file From d990223a8335dc2cbddc421f319da158b68c5393 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 4 Feb 2013 16:36:12 +0100 Subject: [PATCH 100/157] wip bzr revid: nicolas.vanhoren@openerp.com-20130204153612-spv764ylheah3mb2 --- addons/live_support/live_support.py | 7 ++++ addons/live_support/live_support_view.xml | 21 +++++++++- .../live_support/security/ir.model.access.csv | 6 ++- .../security/live_support_security.xml | 12 ++++++ addons/web_im/security/ir.model.access.csv | 4 +- addons/web_im/security/web_im_security.xml | 42 ++++++++++--------- 6 files changed, 66 insertions(+), 26 deletions(-) diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index 8f3ab8e5b31..8f0643e2281 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -27,4 +27,11 @@ class live_support_channel(osv.osv): _name = 'live_support.channel' _columns = { 'name': fields.char(string="Name", size=200, required=True), + 'user_ids': fields.many2many('im.user', 'live_support_channel_im_user', 'channel_id', 'user_id', string="Users"), + } + +class im_user(osv.osv): + _inherit = 'im.user' + _columns = { + 'support_channel_ids': fields.many2many('live_support.channel', 'live_support_channel_im_user', 'user_id', 'channel_id', string="Support Channels"), } diff --git a/addons/live_support/live_support_view.xml b/addons/live_support/live_support_view.xml index e2660d0b51a..05637a40f8f 100644 --- a/addons/live_support/live_support_view.xml +++ b/addons/live_support/live_support_view.xml @@ -8,8 +8,25 @@ live_support.channel tree,form - + + + + + support_channel.form + live_support.channel + +
+ +

+ +

+
+ +
+
+ - \ No newline at end of file diff --git a/addons/live_support/security/ir.model.access.csv b/addons/live_support/security/ir.model.access.csv index b003ba8cd9d..926a3c62451 100644 --- a/addons/live_support/security/ir.model.access.csv +++ b/addons/live_support/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_im_message,live_support.channel,model_live_support_channel,,1,0,0,0 -access_im_message,live_support.channel,model_live_support_channel,group_live_support,1,1,1,1 +access_ls_chann1,live_support.channel,model_live_support_channel,,1,0,0,0 +access_ls_chann2,live_support.channel,model_live_support_channel,group_live_support,1,1,1,1 +access_ls_message,live_support.im.message,web_im.model_im_message,portal.group_anonymous,0,0,0,0 +access_im_user,live_support.im.user,web_im.model_im_user,portal.group_anonymous,1,0,0,0 \ No newline at end of file diff --git a/addons/live_support/security/live_support_security.xml b/addons/live_support/security/live_support_security.xml index 341749626a0..05f10a3fe80 100644 --- a/addons/live_support/security/live_support_security.xml +++ b/addons/live_support/security/live_support_security.xml @@ -12,5 +12,17 @@ The user will be able to join support channels. + + \ No newline at end of file diff --git a/addons/web_im/security/ir.model.access.csv b/addons/web_im/security/ir.model.access.csv index 7333373446a..ed639353e21 100644 --- a/addons/web_im/security/ir.model.access.csv +++ b/addons/web_im/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_im_message,im.message,model_im_message,,1,0,1,0 -access_im_user,im.user,model_im_user,,1,1,1,0 \ No newline at end of file +access_im_message,im.message,model_im_message,base.group_user,1,0,1,0 +access_im_user,im.user,model_im_user,base.group_user,1,1,1,0 \ No newline at end of file diff --git a/addons/web_im/security/web_im_security.xml b/addons/web_im/security/web_im_security.xml index 29983a855d0..a5f71eee58c 100644 --- a/addons/web_im/security/web_im_security.xml +++ b/addons/web_im/security/web_im_security.xml @@ -1,24 +1,26 @@ - - - Can only read messages that you sent or messages sent to you - - ["|", ('to.user', '=', user.id), ('from.user', '=', user.id)] - - - - - + + + Can only read messages that you sent or messages sent to you + + + ["|", ('to.user', '=', user.id), ('from.user', '=', user.id)] + + + + + - - Can only modify your user - - [('user', '=', user.id)] - - - - - - + + Can only modify your user + + + [('user', '=', user.id)] + + + + + + From a0d4b3d099813426905ee271134e9e51726cf1a2 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 4 Feb 2013 17:37:20 +0100 Subject: [PATCH 101/157] Implemented "join a user" bzr revid: nicolas.vanhoren@openerp.com-20130204163720-xpob0dsdrx3idvki --- addons/live_support/live_support.py | 11 +++++++++++ .../live_support/security/live_support_security.xml | 2 +- .../live_support/static/ext/static/js/livesupport.js | 8 ++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index 8f0643e2281..623908ae69c 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -21,6 +21,7 @@ import openerp.addons.web_im.im as im import json +import random from osv import osv, fields class live_support_channel(osv.osv): @@ -30,6 +31,16 @@ class live_support_channel(osv.osv): 'user_ids': fields.many2many('im.user', 'live_support_channel_im_user', 'channel_id', 'user_id', string="Users"), } + def get_available_user(self, cr, uid, channel_id, context=None): + channel = self.browse(cr, uid, channel_id, context=context) + users = [] + for user in channel.user_ids: + if user.im_status: + users.append(user) + if len(users) == 0: + return False + return random.choice(users).id + class im_user(osv.osv): _inherit = 'im.user' _columns = { diff --git a/addons/live_support/security/live_support_security.xml b/addons/live_support/security/live_support_security.xml index 05f10a3fe80..fcf6f71c89f 100644 --- a/addons/live_support/security/live_support_security.xml +++ b/addons/live_support/security/live_support_security.xml @@ -17,7 +17,7 @@ Can only read support users - [('support_channel_ids', '<>', False)] + [('support_channel_ids', '!=', False)] diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index 909aa498ebf..f8282674540 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -25,8 +25,12 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ $.when(templates_def, css_def).then(function() { console.log("starting client"); connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password); - connection.getModel("im.user").search_read([["name", "=", ["Demo User"]]]).then(function(result) { - demo_id = result[0].id; + connection.getModel("live_support.channel").call("get_available_user", [1]).then(function(result) { + if (! result) { + console.log("no available user"); + return; + } + demo_id = result; var manager = new livesupport.ConversationManager(null); manager.start_polling().then(function() { manager.ensure_users([demo_id]).then(function() { From 72eb57f02e750b684717f7dc64dd1cda19e508ec Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 4 Feb 2013 18:12:21 +0100 Subject: [PATCH 102/157] wip bzr revid: nicolas.vanhoren@openerp.com-20130204171221-q6es5fomqbcwzmn0 --- .../static/ext/static/css/livesupport.css | 22 +++++++++++++ .../static/ext/static/js/livesupport.js | 31 ++++++++++++++++--- .../ext/static/js/livesupport_templates.html | 4 +++ .../ext/static/js/livesupport_templates.js | 2 +- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/addons/live_support/static/ext/static/css/livesupport.css b/addons/live_support/static/ext/static/css/livesupport.css index cb4d340134c..3c72981653e 100644 --- a/addons/live_support/static/ext/static/css/livesupport.css +++ b/addons/live_support/static/ext/static/css/livesupport.css @@ -9,6 +9,28 @@ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); } +/* button */ + +.oe_chat_button { + position: fixed; + bottom: 0px; + right: 15px; + display: inline-block; + min-width: 100px; + background-color: rgba(60, 60, 60, 0.6); + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; + font-size: 14px; + font-weight: bold; + padding: 10px; + color: white; + text-shadow: rgb(59, 76, 88) 1px 1px 0px; + border: 1px solid rgb(80, 80, 80); + border-bottom: 0px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + cursor: pointer; +} + /* conversations */ .oe_im_chatview { diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index f8282674540..214ba6230d5 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -6,7 +6,11 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ templateEngine.extendEnvironment({"toUrl": _.bind(require.toUrl, require)}); var connection; - livesupport.main = function(server_url, db, login, password) { + livesupport.main = function(server_url, db, login, password, options) { + options = options || {}; + _.defaults(options, { + buttonText: "Chat with one of our collaborators", + }); var templates_def = $.ajax({ url: require.toUrl("./livesupport_templates.js"), jsonp: false, @@ -25,6 +29,25 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ $.when(templates_def, css_def).then(function() { console.log("starting client"); connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password); + new livesupport.ChatButton(null, options.buttonText).appendTo($("body")); + }); + }; + + var ERROR_DELAY = 5000; + + livesupport.ChatButton = nova.Widget.$extend({ + className: "openerp_style oe_chat_button", + events: { + "click": "click", + }, + __init__: function(parent, text) { + this.$super(parent); + this.text = text; + }, + render: function() { + this.$().append(templateEngine.chatButton({widget: this})); + }, + click: function() { connection.getModel("live_support.channel").call("get_available_user", [1]).then(function(result) { if (! result) { console.log("no available user"); @@ -38,10 +61,8 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ }); }); }); - }); - }; - - var ERROR_DELAY = 5000; + }, + }); livesupport.ImUser = nova.Class.$extend({ __include__: [nova.DynamicProperties], diff --git a/addons/live_support/static/ext/static/js/livesupport_templates.html b/addons/live_support/static/ext/static/js/livesupport_templates.html index 96eecd2c21a..aa3b24ad491 100644 --- a/addons/live_support/static/ext/static/js/livesupport_templates.html +++ b/addons/live_support/static/ext/static/js/livesupport_templates.html @@ -29,4 +29,8 @@
${time}
+ + +<%def name="chatButton"> + ${widget.text} \ No newline at end of file diff --git a/addons/live_support/static/ext/static/js/livesupport_templates.js b/addons/live_support/static/ext/static/js/livesupport_templates.js index 0e26101ba97..c671a6f1edc 100644 --- a/addons/live_support/static/ext/static/js/livesupport_templates.js +++ b/addons/live_support/static/ext/static/js/livesupport_templates.js @@ -1 +1 @@ -window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n
\n \n ${widget.user.get('name') || 'You'}\n \n
\n
\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n
\n
\n
\n
\n
\n \n
\n\n\n<%def name=\"conversation_bubble\">\n
\n
\n \n
\n
${user.get('name') || 'You'}
\n
\n % _.each(items, function(item) {\n
${item}
\n % });\n
\n
${time}
\n
\n"); +window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n
\n \n ${widget.user.get('name') || 'You'}\n \n
\n
\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n
\n
\n
\n
\n
\n \n
\n\n\n<%def name=\"conversation_bubble\">\n
\n
\n \n
\n
${user.get('name') || 'You'}
\n
\n % _.each(items, function(item) {\n
${item}
\n % });\n
\n
${time}
\n
\n\n\n<%def name=\"chatButton\">\n ${widget.text}\n"); From 332a4b32b35734617e31fc3acb9e8fdac04df82d Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 4 Feb 2013 18:29:14 +0100 Subject: [PATCH 103/157] perfected customer button bzr revid: nicolas.vanhoren@openerp.com-20130204172914-ojc9j501vqf3p6ub --- .../static/ext/static/css/livesupport.css | 4 +- .../static/ext/static/js/livesupport.js | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/addons/live_support/static/ext/static/css/livesupport.css b/addons/live_support/static/ext/static/css/livesupport.css index 3c72981653e..01a918c8b7f 100644 --- a/addons/live_support/static/ext/static/css/livesupport.css +++ b/addons/live_support/static/ext/static/css/livesupport.css @@ -14,7 +14,7 @@ .oe_chat_button { position: fixed; bottom: 0px; - right: 15px; + right: 6px; display: inline-block; min-width: 100px; background-color: rgba(60, 60, 60, 0.6); @@ -36,7 +36,7 @@ .oe_im_chatview { position: fixed; overflow: hidden; - bottom: 6px; + bottom: 42px; margin-right: 6px; background: rgba(60, 60, 60, 0.8); -moz-border-radius: 3px; diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index 214ba6230d5..c0ad2e1be5d 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -48,16 +48,35 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ this.$().append(templateEngine.chatButton({widget: this})); }, click: function() { - connection.getModel("live_support.channel").call("get_available_user", [1]).then(function(result) { - if (! result) { - console.log("no available user"); + if (! this.manager) { + this.manager = new livesupport.ConversationManager(null); + this.activated_def = this.manager.start_polling(); + } + var def = $.Deferred(); + $.when(this.activated_def).then(function() { + def.resolve(); + }, function() { + def.reject(); + }); + setTimeout(function() { + def.reject(); + }, 5000); + def.then(_.bind(this.chat, this), function() { + window.alert("It seems the connection to the server is encountering problems, please try again later."); + }); + }, + chat: function() { + var self = this; + if (this.manager.conversations.length > 0) + return; + connection.getModel("live_support.channel").call("get_available_user", [1]).then(function(user_id) { + if (! user_id) { + window.alert("None of our collaborators seems to be available, please try again later."); return; } - demo_id = result; - var manager = new livesupport.ConversationManager(null); - manager.start_polling().then(function() { - manager.ensure_users([demo_id]).then(function() { - manager.activate_user(manager.get_user(demo_id)); + self.manager.start_polling().then(function() { + self.manager.ensure_users([user_id]).then(function() { + self.manager.activate_user(self.manager.get_user(user_id)); }); }); }); @@ -350,7 +369,7 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ destroy: function() { this.user.remove_watcher(); this.trigger("destroyed"); - return this._super(); + return this.$super(); }, }); From e2a753901d4151252000b305242822fd1b6fa204 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 11:30:40 +0100 Subject: [PATCH 104/157] minor modif bzr revid: nicolas.vanhoren@openerp.com-20130205103040-3jbbdab4soxkrsaj --- .../static/ext/static/js/livesupport_templates.html | 2 +- .../live_support/static/ext/static/js/livesupport_templates.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/live_support/static/ext/static/js/livesupport_templates.html b/addons/live_support/static/ext/static/js/livesupport_templates.html index aa3b24ad491..660a07a45ce 100644 --- a/addons/live_support/static/ext/static/js/livesupport_templates.html +++ b/addons/live_support/static/ext/static/js/livesupport_templates.html @@ -12,7 +12,7 @@
diff --git a/addons/live_support/static/ext/static/js/livesupport_templates.js b/addons/live_support/static/ext/static/js/livesupport_templates.js index c671a6f1edc..3f133b594c7 100644 --- a/addons/live_support/static/ext/static/js/livesupport_templates.js +++ b/addons/live_support/static/ext/static/js/livesupport_templates.js @@ -1 +1 @@ -window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n
\n \n ${widget.user.get('name') || 'You'}\n \n
\n
\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n
\n
\n
\n
\n
\n \n
\n\n\n<%def name=\"conversation_bubble\">\n
\n
\n \n
\n
${user.get('name') || 'You'}
\n
\n % _.each(items, function(item) {\n
${item}
\n % });\n
\n
${time}
\n
\n\n\n<%def name=\"chatButton\">\n ${widget.text}\n"); +window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n
\n \n ${widget.user.get('name') || 'You'}\n \n
\n
\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n
\n
\n
\n
\n
\n \n
\n\n\n<%def name=\"conversation_bubble\">\n
\n
\n \n
\n
${user.get('name') || 'You'}
\n
\n % _.each(items, function(item) {\n
${item}
\n % });\n
\n
${time}
\n
\n\n\n<%def name=\"chatButton\">\n ${widget.text}\n"); From f5f3fc7584987438dac6985ad49a205b2014e2fb Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 11:35:22 +0100 Subject: [PATCH 105/157] now uses local storage bzr revid: nicolas.vanhoren@openerp.com-20130205103522-sapld2hhpgjda81a --- addons/live_support/static/ext/static/js/livesupport.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index c0ad2e1be5d..e3523298274 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -140,7 +140,14 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ start_polling: function() { var self = this; - return connection.connector.call("/longpolling/im/gen_uuid", {}).then(function(uuid) { + var uuid = localStorage["oe_livesupport_uuid"]; + var def = $.when(uuid); + + if (! uuid) { + def = connection.connector.call("/longpolling/im/gen_uuid", {}); + } + return def.then(function(uuid) { + localStorage["oe_livesupport_uuid"] = uuid; return connection.getModel("im.user").call("get_by_user_id", [uuid]).then(function(my_id) { self.my_id = my_id["id"]; return self.ensure_users([self.my_id]).then(function() { From cbcbe926a4109598ad57532c329e4031ccadad92 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 14:47:11 +0100 Subject: [PATCH 106/157] working 2-lines inclusion bzr revid: nicolas.vanhoren@openerp.com-20130205134711-h0g2pw9f9b1nyh7j --- addons/live_support/live_support.py | 29 +++++++++++++++++++ addons/live_support/loader.js | 15 ++++++++++ .../static/ext/static/js/livesupport.js | 9 +++--- addons/live_support/static/ext/test.html | 19 ++---------- 4 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 addons/live_support/loader.js diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index 623908ae69c..a5ed05e40ed 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -19,11 +19,35 @@ # ############################################################################## +import openerp import openerp.addons.web_im.im as im import json import random +import jinja2 from osv import osv, fields +env = jinja2.Environment( + loader=jinja2.PackageLoader('openerp.addons.live_support', "."), + autoescape=False +) +env.filters["json"] = json.dumps + +class ImportController(openerp.addons.web.http.Controller): + _cp_path = '/live_support' + + @openerp.addons.web.http.httprequest + def loader(self, req, **kwargs): + db = kwargs["db"] + channel = int(kwargs["channel"]) + req.session._db = db + req.session._uid = None + req.session._login = "anonymous" + req.session._password = "anonymous" + info = req.session.model('live_support.channel').get_info_for_chat_src() + info["db"] = db + info["channel"] = channel + return env.get_template("loader.js").render(info) + class live_support_channel(osv.osv): _name = 'live_support.channel' _columns = { @@ -41,6 +65,11 @@ class live_support_channel(osv.osv): return False return random.choice(users).id + def get_info_for_chat_src(self, cr, uid, context=None): + url = self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url') + return {"url": url} + + class im_user(osv.osv): _inherit = 'im.user' _columns = { diff --git a/addons/live_support/loader.js b/addons/live_support/loader.js new file mode 100644 index 00000000000..89f89b56b90 --- /dev/null +++ b/addons/live_support/loader.js @@ -0,0 +1,15 @@ + +require.config({ + context: "oelivesupport", + baseUrl: {{url | json}} + "/live_support/static/ext/static/js", + shim: { + underscore: { + init: function() { + return _.noConflict(); + }, + }, + }, +})(["livesupport", "jquery"], function(livesupport, jQuery) { + jQuery.noConflict(); + livesupport.main({{url | json}}, {{db | json}}, "anonymous", "anonymous", {{channel | json}}); +}); diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index e3523298274..699cfa6d375 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -6,7 +6,7 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ templateEngine.extendEnvironment({"toUrl": _.bind(require.toUrl, require)}); var connection; - livesupport.main = function(server_url, db, login, password, options) { + livesupport.main = function(server_url, db, login, password, channel, options) { options = options || {}; _.defaults(options, { buttonText: "Chat with one of our collaborators", @@ -29,7 +29,7 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ $.when(templates_def, css_def).then(function() { console.log("starting client"); connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password); - new livesupport.ChatButton(null, options.buttonText).appendTo($("body")); + new livesupport.ChatButton(null, channel, options.buttonText).appendTo($("body")); }); }; @@ -40,8 +40,9 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ events: { "click": "click", }, - __init__: function(parent, text) { + __init__: function(parent, channel, text) { this.$super(parent); + this.channel = channel; this.text = text; }, render: function() { @@ -69,7 +70,7 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ var self = this; if (this.manager.conversations.length > 0) return; - connection.getModel("live_support.channel").call("get_available_user", [1]).then(function(user_id) { + connection.getModel("live_support.channel").call("get_available_user", [this.channel]).then(function(user_id) { if (! user_id) { window.alert("None of our collaborators seems to be available, please try again later."); return; diff --git a/addons/live_support/static/ext/test.html b/addons/live_support/static/ext/test.html index 68fd2dfa1f8..05ac53c41e7 100644 --- a/addons/live_support/static/ext/test.html +++ b/addons/live_support/static/ext/test.html @@ -1,23 +1,8 @@ - - + + From 9426ef1f1ee0eeaf1e3cff059288fb713084c992 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 14:53:25 +0100 Subject: [PATCH 107/157] wip bzr revid: nicolas.vanhoren@openerp.com-20130205135325-am3e3yda385qkh2z --- addons/live_support/live_support.py | 8 +++++--- addons/live_support/static/ext/test.html | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index a5ed05e40ed..530e171d455 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -37,8 +37,9 @@ class ImportController(openerp.addons.web.http.Controller): @openerp.addons.web.http.httprequest def loader(self, req, **kwargs): - db = kwargs["db"] - channel = int(kwargs["channel"]) + p = json.loads(kwargs["p"]) + db = p["db"] + channel = p["channel"] req.session._db = db req.session._uid = None req.session._login = "anonymous" @@ -46,7 +47,8 @@ class ImportController(openerp.addons.web.http.Controller): info = req.session.model('live_support.channel').get_info_for_chat_src() info["db"] = db info["channel"] = channel - return env.get_template("loader.js").render(info) + return req.make_response(env.get_template("loader.js").render(info), + headers=[('Content-Type', "text/javascript")]) class live_support_channel(osv.osv): _name = 'live_support.channel' diff --git a/addons/live_support/static/ext/test.html b/addons/live_support/static/ext/test.html index 05ac53c41e7..17d067500db 100644 --- a/addons/live_support/static/ext/test.html +++ b/addons/live_support/static/ext/test.html @@ -2,7 +2,7 @@ - + From 8c0530193ae85e4326a99389a577dd32d3529378 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 15:01:43 +0100 Subject: [PATCH 108/157] now only display button if there is someone available bzr revid: nicolas.vanhoren@openerp.com-20130205140143-nughxscrgtyzjlht --- addons/live_support/live_support.py | 8 ++++++++ addons/live_support/static/ext/static/js/livesupport.js | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index 530e171d455..08291666e9d 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -50,6 +50,14 @@ class ImportController(openerp.addons.web.http.Controller): return req.make_response(env.get_template("loader.js").render(info), headers=[('Content-Type', "text/javascript")]) + @openerp.addons.web.http.jsonrequest + def available(self, req, db, channel): + req.session._db = db + req.session._uid = None + req.session._login = "anonymous" + req.session._password = "anonymous" + return req.session.model('live_support.channel').get_available_user(channel) > 0 + class live_support_channel(osv.osv): _name = 'live_support.channel' _columns = { diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index 699cfa6d375..a80dc63ac94 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -27,9 +27,13 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ }); $.when(templates_def, css_def).then(function() { - console.log("starting client"); + console.log("starting live support customer app"); connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password); - new livesupport.ChatButton(null, channel, options.buttonText).appendTo($("body")); + connection.connector.call("/live_support/available", {db: db, channel: channel}).then(function(activated) { + if (! activated) + return; + new livesupport.ChatButton(null, channel, options.buttonText).appendTo($("body")); + }); }); }; From 7472edcdef853c833d2c33686d22209cce2d52a0 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 15:24:09 +0100 Subject: [PATCH 109/157] fixed double poll problem bzr revid: nicolas.vanhoren@openerp.com-20130205142409-haqewgtmjr5j1xy0 --- addons/live_support/static/ext/static/js/livesupport.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/addons/live_support/static/ext/static/js/livesupport.js b/addons/live_support/static/ext/static/js/livesupport.js index a80dc63ac94..3f5238310e5 100644 --- a/addons/live_support/static/ext/static/js/livesupport.js +++ b/addons/live_support/static/ext/static/js/livesupport.js @@ -79,10 +79,8 @@ define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $ window.alert("None of our collaborators seems to be available, please try again later."); return; } - self.manager.start_polling().then(function() { - self.manager.ensure_users([user_id]).then(function() { - self.manager.activate_user(self.manager.get_user(user_id)); - }); + self.manager.ensure_users([user_id]).then(function() { + self.manager.activate_user(self.manager.get_user(user_id)); }); }); }, From b374e879902b5ac5b0a6a65ce2851a0781f42af1 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 5 Feb 2013 15:52:54 +0100 Subject: [PATCH 110/157] made nicer menu for support side bzr revid: nicolas.vanhoren@openerp.com-20130205145254-c50ievz588bo2olz --- addons/live_support/live_support.py | 22 ++++++++++++++++++++++ addons/live_support/live_support_view.xml | 9 +++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/addons/live_support/live_support.py b/addons/live_support/live_support.py index 08291666e9d..67b08a1987b 100644 --- a/addons/live_support/live_support.py +++ b/addons/live_support/live_support.py @@ -60,9 +60,21 @@ class ImportController(openerp.addons.web.http.Controller): class live_support_channel(osv.osv): _name = 'live_support.channel' + + def _are_you_inside(self, cr, uid, ids, name, arg, context=None): + res = {} + for record in self.browse(cr, uid, ids, context=context): + res[record.id] = False + for user in record.user_ids: + if user.user.id == uid: + res[record.id] = True + break + return res + _columns = { 'name': fields.char(string="Name", size=200, required=True), 'user_ids': fields.many2many('im.user', 'live_support_channel_im_user', 'channel_id', 'user_id', string="Users"), + 'are_you_inside': fields.function(_are_you_inside, type='boolean', string='Are you inside the matrix?', store=False), } def get_available_user(self, cr, uid, channel_id, context=None): @@ -79,6 +91,16 @@ class live_support_channel(osv.osv): url = self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url') return {"url": url} + def join(self, cr, uid, ids, context=None): + my_id = self.pool.get("im.user").get_by_user_id(cr, uid, uid, context)["id"] + self.write(cr, uid, ids, {'user_ids': [(4, my_id)]}) + return True + + def quit(self, cr, uid, ids, context=None): + my_id = self.pool.get("im.user").get_by_user_id(cr, uid, uid, context)["id"] + self.write(cr, uid, ids, {'user_ids': [(3, my_id)]}) + return True + class im_user(osv.osv): _inherit = 'im.user' diff --git a/addons/live_support/live_support_view.xml b/addons/live_support/live_support_view.xml index 05637a40f8f..cdcf5cb9e48 100644 --- a/addons/live_support/live_support_view.xml +++ b/addons/live_support/live_support_view.xml @@ -20,8 +20,13 @@

-