From cb1f62883b47933e18e935abefb890d595ada31e Mon Sep 17 00:00:00 2001 From: tonio Date: Thu, 4 Mar 2010 17:55:28 +0100 Subject: [PATCH] Big webmail update: * Dragging multiple messages is now possible * Better email displaying (headers and body are now displayed at the same time...) * Bugfixes about dropdown menus * Misc. enhancements. --- extensions/webmail/forms.py | 1 + extensions/webmail/imap_listing.py | 21 +- extensions/webmail/main.py | 63 ++-- extensions/webmail/templates/compose.html | 10 +- extensions/webmail/templates/headers.html | 4 +- extensions/webmail/templates/index.html | 161 ++++++--- extensions/webmail/templates/viewmail.html | 67 ++-- extensions/webmail/templatetags/webextras.py | 18 +- lib/email_listing.py | 14 +- lib/tables.py | 9 +- static/js/common.js | 20 +- static/js/mootools-more.js | 340 ++++++++++++------- static/stylesheets/default.css | 44 ++- templates/common/email_listing.html | 12 +- templates/common/table_head.html | 6 +- templates/main/menu.html | 8 +- templates/nlayout.html | 23 +- 17 files changed, 524 insertions(+), 297 deletions(-) diff --git a/extensions/webmail/forms.py b/extensions/webmail/forms.py index 21c2bd86f..a57423774 100644 --- a/extensions/webmail/forms.py +++ b/extensions/webmail/forms.py @@ -4,5 +4,6 @@ from django.utils.translation import ugettext as _ class ComposeMailForm(forms.Form): from_ = forms.CharField(label=_("From")) to = forms.CharField(label=_("To"), widget=forms.Textarea()) + cc = forms.CharField(label=_("Cc"), widget=forms.Textarea()) subject = forms.CharField(label=_("Subject"), max_length=255) diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py index 8656cda15..92c84d1d4 100644 --- a/extensions/webmail/imap_listing.py +++ b/extensions/webmail/imap_listing.py @@ -9,7 +9,7 @@ from email.header import decode_header import email.utils from django.utils.translation import ugettext as _ from mailng.lib import decode, tables, imap_utf7, Singleton -from mailng.lib.email_listing import MBconnector, EmailListing +from mailng.lib.email_listing import MBconnector, EmailListing, Email from mailng.lib import tables, imap_utf7, parameters @@ -17,7 +17,8 @@ class WMtable(tables.Table): tableid = "emails" idkey = "imapid" # selection = tables.SelectionColumn("selection", width="20px", first=True) - _1_flags = tables.ImgColumn("flags", width="6%") + _1_flags = tables.ImgColumn("flags", width="4%", + header="") _2_subject = tables.Column("subject", label=_("Subject"), cssclass="draggable", width="50%") _3_from_ = tables.Column("from", width="20%", label=_("From")) @@ -226,3 +227,19 @@ class ImapListing(EmailListing): for fd in folders: md_folders += [{"name" : fd}] return md_folders + +class ImapEmail(Email): + def __init__(self, msg, **kwargs): + Email.__init__(self, msg, **kwargs) + + fields = ["Subject", "From", "To", "Cc", "Date"] + for f in fields: + label = f + if not f in msg.keys(): + f = f.upper() + if not f in msg.keys(): + continue + try: + self.headers += [{"name" : label, "value" : getattr(IMAPheader, "parse_%s" % f.lower())(msg[f])}] + except AttributeError: + self.headers += [{"name" : label, "value" : msg[f]}] diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index 2f5a3c058..69bf4d0db 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -2,6 +2,7 @@ import time import sys from django.http import HttpResponse +from django.template import Template, Context from django.template.loader import render_to_string from django.utils import simplejson from django.utils.translation import ugettext as _ @@ -10,10 +11,10 @@ from django.conf.urls.defaults import include from django.contrib.auth.decorators import login_required from mailng.admin.models import Mailbox from mailng.lib import events, parameters, _render, _render_error -from mailng.lib.email_listing import Email from imap_listing import * from forms import * from templatetags.webextras import * +from imap_listing import ImapEmail def init(): events.register("UserMenuDisplay", menu) @@ -65,6 +66,9 @@ def folder(request, name, updatenav=True): request.session["page"] = pageid if order: request.session["navparams"]["order"] = order + else: + # Internal usage + order = request.session["navparams"]["order"] lst = ImapListing(request.user.username, request.session["password"], name, folder=name, order=order) @@ -98,42 +102,45 @@ def fetchmail(request, folder, mail_id, all=False): @login_required def viewmail(request, folder, mail_id=None): - header = fetchmail(request, folder, mail_id) - if header: - from templatetags.webextras import viewm_menu - try: - pageid = request.session["page"] - except KeyError: - pageid = "1" - menu = viewm_menu("", request.session, mail_id, - request.user.get_all_permissions()) - folder, imapid = header["imapid"].split("/") - mailcontent = render_to_string("webmail/viewmail.html", { - "header" : header, "folder" : folder, "imapid" : imapid - }) - ctx = getctx("ok", menu=menu, listing=mailcontent) - return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + from templatetags.webextras import viewm_menu + + content = Template(""" + +""").render(Context({"url" : reverse(getmailcontent, args=[folder, mail_id])})) + menu = viewm_menu("", request.session, mail_id, + request.user.get_all_permissions()) + ctx = getctx("ok", menu=menu, listing=content) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required def getmailcontent(request, folder, mail_id): - from mailng.lib.email_listing import Email - - msg = fetchmail(request, folder, mail_id, True) - if "class" in msg.keys() and msg["class"] == "unseen": - IMAPconnector(request).msgseen(msg["imapid"]) - email = Email(msg, mode="html", links="1") - return email.render(request) + msg = fetchmail(request, folder, mail_id, True) + if "class" in msg.keys() and msg["class"] == "unseen": + IMAPconnector(request).msgseen(msg["imapid"]) + email = ImapEmail(msg, mode="html", links="1") + try: + pageid = request.session["page"] + except KeyError: + pageid = "1" + folder, imapid = msg["imapid"].split("/") + return _render(request, "webmail/viewmail.html", { + "headers" : email.render_headers(), "folder" : folder, + "imapid" : imapid, "mailbody" : email.body, "pre" : email.pre + }) @login_required def move(request): for arg in ["msgset", "to"]: if not request.GET.has_key(arg): return - folder, id = request.GET["msgset"].split("/") + msgset = [] + fdname = "" + for item in request.GET["msgset"].split(","): + fdname, id = item.split("/") + msgset += [id] mbc = IMAPconnector(request) - mbc.move(id, folder, request.GET["to"]) - return HttpResponse(simplejson.dumps({"status" : "ok"}), - mimetype="application/json") + mbc.move(",".join(msgset), fdname, request.GET["to"]) + return folder(request, fdname, False) @login_required def empty(request, name): @@ -176,7 +183,7 @@ def reply(request, folder, mail_id): if request.method == "POST": return msg = fetchmail(request, folder, mail_id, True) - email = Email(msg) + email = ImapEmail(msg) lines = email.body.split('\n') body = "" for l in lines: diff --git a/extensions/webmail/templates/compose.html b/extensions/webmail/templates/compose.html index ef6a5e458..01bb05acd 100644 --- a/extensions/webmail/templates/compose.html +++ b/extensions/webmail/templates/compose.html @@ -2,11 +2,11 @@ diff --git a/extensions/webmail/templates/headers.html b/extensions/webmail/templates/headers.html index 244d108df..4c06206cc 100644 --- a/extensions/webmail/templates/headers.html +++ b/extensions/webmail/templates/headers.html @@ -1,5 +1,5 @@ - +
{% for hdr in headers %} - + {% endfor %}
{{ hdr.name }}{{ hdr.value }}
{{ hdr.name }}{{ hdr.value }}
diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html index 9417c65a6..fa3f6354f 100644 --- a/extensions/webmail/templates/index.html +++ b/extensions/webmail/templates/index.html @@ -23,7 +23,7 @@ table#emails > tbody { } #emails th { - text-align: center; + /*text-align: center;*/ } #emails tr { @@ -69,20 +69,16 @@ table#emails > tbody { border: 0; } -td { - position: relative; -} - th { - -moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; - -webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; + /*-moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; + -webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset;*/ } th.selected { background: #EEEEEE; } -.draggable { +.draggable {2 cursor: move; } @@ -91,8 +87,14 @@ th.selected { } tr { - cursor: pointer; + position: relative; + border-bottom: 1px solid #C8D6E8; } + +tr.selected { + background: #ffffcc; +} + {% endblock %} {% block js %} navparams = new Hash({ @@ -101,21 +103,21 @@ tr { {% endfor %} }); - updatelisting = function(resp, overflow) { - /*if (!$defined(overflow)) { - overflow = "auto"; - }*/ + updatelisting = function(resp) { if ($defined(resp.menu)) { $("menubar").set("html", resp.menu); + parse_menubar("menubar"); } $("listing").set("html", resp.listing); - /*if ($defined(resp.navbar)) { + if ($defined(resp.navbar)) { $("navbar").set("html", resp.navbar); - $("navbar").setStyle("display", "block"); $("navbar").getChildren("a").addEvent("click", navbarcallback); - }*/ + } $$("a[name=compose]").addEvent("click", loadCompose); $$("a[name=empty]").addEvent("click", empty); + if (!checkSelection()) { + select($$("a[href=INBOX]")); + } } empty = function(event) { @@ -125,6 +127,17 @@ tr { }}).get(); } + checkSelection = function() { + var selection = false; + + $$("a[name=loadfolder]").each(function(item) { + if (!selection) { + selection = item.getParent().hasClass("selected"); + } + }); + return selection; + } + select = function(obj) { $$("a[name=loadfolder]").each(function(item) { item.getParent().removeClass("selected"); @@ -147,23 +160,27 @@ tr { updatelisting(resp, "hidden"); $$("a[name=back]").addEvent("click", loadFolder); $$("a[name=reply]").addEvent("click", reply); - setDivHeight("mailcontent", $("table-container").getSize().y, 0); + setDivHeight("mailcontent", /*$("table-container").getSize().y*/0, 0); }); compose_callback = function(resp) { - updatelisting(resp, "hidden"); + updatelisting(resp); + window.addEvent("resize", function(evt) { + var bodysize = $("listing").getSize().y - $("mailheader").getSize().y; + $("id_body").setStyle("height", bodysize + "px"); + }); $$("a[name=back]").addEvent("click", loadFolder); $$("a[name=sendmail]").addEvent("click", function(evt) { evt.stop(); $("composemail").set("send", { onSuccess: function(res) { updatelisting(JSON.decode(res)); - $$("tr").addEvent("click", viewmail); + $$("tr").addEvent("dblclick", viewmail); } }); $("composemail").send(); }); - setDivHeight("id_body", $("mailheader").getSize().y + 20, 0); + window.fireEvent("resize"); } loadCompose = function(event) { @@ -187,17 +204,60 @@ tr { current_anchor.update(); } register_callback("folder", function(resp) { - updatelisting(resp, "auto"); + updatelisting(resp); if (navparams.has("order")) { src = navparams.get("order").charAt(0) == "-" ? "down_arrow.png" : "up_arrow.png"; th = $$("th[id=" + navparams.get("order").substring(1) + "]"); th.getElement("img").set("src", "/static/pics/" + src); th.addClass("selected"); } - /*$$("td[class*=draggable]").each(function(item) { - item.makeDraggable({ + var selectedRows = new Array(); + $$("tr").addEvent("click", function(event) { + var tr = event.target.getParent(); + if (!tr.hasClass("selected")) { + tr.addClass("selected"); + selectedRows.include(tr); + } else { + tr.removeClass("selected"); + selectedRows.erase(tr); + } + }); + + $$("tr").each(function(item) { + new Drag.Move(item, { droppables: ".droppable", }).addEvents({ + "start" : function(item) { + if (!item.hasClass("selected")) { + selectedRows.each(function(item, index) { + item.removeClass("selected"); + }); + selectedRows.empty(); + item.addClass("selected"); + selectedRows.include(item); + } + var block = new Element("div", {id: "draggeditems"}) + .setStyles({"position" : "absolute", + "opacity" : 0.8, + "width" : "200px", + "padding" : "5px", + "text-align" : "center", + "height" : "25px", + "border" : "1px solid #333", + "background" : "white"}) + .setStyles(item.getPosition()) + .inject(document.body); + block.set("html", "Moving " + selectedRows.length + " messages"); + this.oldelement = this.element; + this.element = block; + }, + "cancel" : function(item) { + if ($defined(this.oldelement)) { + item.dispose(); + delete(item); + this.element = this.oldelement; + } + }, "enter" : function(item, droppable) { droppable.addClass("dragging"); }, @@ -212,35 +272,32 @@ tr { return; } droppable.removeClass("dragging"); - this.oldelement.getParent().dispose(); - to = droppable.getFirst("a").get("href"); + dest = droppable.getFirst("a").get("href"); + msgset = new Array(); + selectedRows.each(function(item, index) { + msgset.include(item.get("id")); + }); {% url extensions.webmail.main.move as moveurl %} - new Request.JSON({url: "{{ moveurl }}", onSuccess: function(resp) { - - }}).get({msgset : this.oldelement.getParent().get("id"), - to : to}); + new Request.JSON({url: "{{ moveurl }}", + onSuccess: get_callback("folder")} + ).get({msgset : msgset.join(), to : dest}); }, - "beforeStart": function(item) { - var clone = item.clone() - .setStyles({"position" : "absolute", - "opacity" : 0.6, - "width" : "300px", - "border" : "1px solid #333", - "background" : "white"}) - .setStyles(item.getCoordinates()) - .inject(document.body); - this.oldelement = this.element; - this.element = clone; - }, - "cancel": function(item) { - if ($defined(this.oldelement)) { - item.dispose(); - delete(item); - this.element = this.oldelement; - } - } }); - });*/ + }); + $("toggleselect").addEvent("click", function(evt) { + var checked = evt.target.get("checked"); + if (checked) { + $$("tbody>tr").each(function(item) { + item.addClass("selected"); + selectedRows.include(item); + }); + } else { + $$("tr").each(function(item) { item.removeClass("selected"); }); + selectedRows.empty(); + } + }); + + $$("tr").addEvent("dblclick", viewmail); $$("th[class*=sortable]").addEvent("click", function(evt) { var tmp = "?"; @@ -260,11 +317,11 @@ tr { }); }); - /*navbarcallback = function(event) { + navbarcallback = function(event) { event.stop(); current_anchor.setparams(navparams); current_anchor.parse_string(event.target.getParent().get("href")).update(); - }*/ + } foldercallback = function(event) { select(event.target); @@ -273,8 +330,6 @@ tr { ajaxListener("INBOX/", updatelisting); $$("a[name=loadfolder]").addEvent("click", foldercallback); - /*$("navbar").getChildren("a").addEvent("click", navbarcallback);*/ - {% endblock %} {% block middletitle %}{% trans "Webmail" %}{% endblock %} {% block leftcol %} diff --git a/extensions/webmail/templates/viewmail.html b/extensions/webmail/templates/viewmail.html index 97637a322..5830936cd 100644 --- a/extensions/webmail/templates/viewmail.html +++ b/extensions/webmail/templates/viewmail.html @@ -1,25 +1,44 @@ {% load i18n %} -{% load webextras %} - - -
- {% render_headers header %} -
- -{% url extensions.webmail.main.getmailcontent folder imapid as geturl %} - + + + + + + + + + +
+ {{ headers }} +
+ {% if pre %}
{% endif %}{{ mailbody }}{% if pre %}
{% endif %} + + diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 826424f01..b371fa337 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -70,7 +70,7 @@ def listing_menu(selection, folder, perms): {"name" : "compose", "url" : reverse(webmail.main.compose), "img" : "/static/pics/edit.png", - "label" : _("New message")} + "label" : _("New message")}, ] if folder in ["Trash"]: entries += [ @@ -83,19 +83,3 @@ def listing_menu(selection, folder, perms): return render_to_string("main/menu.html", {"selection" : selection, "entries" : entries, "perms" : perms}) - -@register.simple_tag -def render_headers(headers): - fields = ["Subject", "From", "To", "Cc", "Date"] - result = [] - for f in fields: - if not f in headers.keys(): - continue - try: - result += [{"name" : f, "value" : getattr(IMAPheader, "parse_%s" % f.lower())(headers[f])}] - except AttributeError: - result += [{"name" : f, "value" : headers[f]}] - return render_to_string("webmail/headers.html", { - "headers" : result - }) - diff --git a/lib/email_listing.py b/lib/email_listing.py index c6c827fab..c879d1371 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -3,6 +3,7 @@ import time import re import lxml import lxml.html +from lxml import etree from django.template.loader import render_to_string from django.template import Template, Context from mailng.lib import _render, decode @@ -124,6 +125,7 @@ class Email(object): def __init__(self, msg, mode="plain", links="0"): self.attached_map = {} contents = {"html" : "", "plain" : ""} + self.headers = [] for part in msg.walk(): if part.get_content_maintype() == 'multipart': continue @@ -166,6 +168,11 @@ class Email(object): return map[m.group(1)] return url + def render_headers(self): + return render_to_string("webmail/headers.html", { + "headers" : self.headers + }) + def render(self, request): return _render(request, 'common/getmailcontent.html', { "body" : self.body, "pre" : self.pre @@ -182,6 +189,11 @@ class Email(object): html.rewrite_links(lambda x: None) else: html.rewrite_links(self.map_cid) - body = lxml.html.tostring(html) + body = html.find("body") + if not body: + body = lxml.html.tostring(html) + else: + body = lxml.html.tostring(body) + body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) body = Template(decode(body)).render({}) return (False, body) diff --git a/lib/tables.py b/lib/tables.py index 938df9cad..3a3e07f33 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -5,6 +5,8 @@ from django.template import Template, Context from django.template.loader import render_to_string class Column: + sortable = True + def __init__(self, name, **kwargs): self.name = name for k, v in kwargs.iteritems(): @@ -26,8 +28,13 @@ class SelectionColumn(Column): % (self.name, value) class ImgColumn(Column): + sortable = False + def __str__(self): - return "" + try: + return getattr(self, "header") + except AttributeError: + return "" def render(self, value): return "" % value diff --git a/static/js/common.js b/static/js/common.js index b859c883a..46c13f0df 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -1,9 +1,8 @@ window.addEvent('domready', function(){ parse_menubar('topmenubar'); - parse_menubar('menubar'); - /*SqueezeBox.assign($$('a.boxed'), { + SqueezeBox.assign($$('a.boxed'), { parse: 'rel', - });*/ + }); }); callbacks = {}; @@ -12,6 +11,10 @@ register_callback = function(name, callback) { callbacks[name] = callback; } +get_callback = function(name) { + return callbacks[name]; +} + current_anchor = null; check_anchor = function() { @@ -134,9 +137,16 @@ function HashWrapper(deflocation) { parse_menubar = function(id) { if ($(id)) { - $(id).getElements('li.dropdown').each(function( elem ){ + $(id).getElements('li.dropdown').each(function(elem){ var list = elem.getElement('ul.links'); - var myFx = new Fx.Slide(list).hide(); + var myFx = new Fx.Slide(list, { + duration: 200 + }).hide(); + + myFx.addEvent("start", function() { + this.wrapper.setStyle("clear", "both"); + }); + elem.addEvents({ 'mouseenter' : function(){ myFx.cancel(); diff --git a/static/js/mootools-more.js b/static/js/mootools-more.js index 3a4b1bffc..58b45bc8c 100644 --- a/static/js/mootools-more.js +++ b/static/js/mootools-more.js @@ -23,8 +23,8 @@ provides: [MooTools.More] */ MooTools.More = { - 'version': '1.2.4.2', - 'build': 'bd5a93c0913cce25917c48cbdacde568e15e02ef' + 'version': '1.2.4.4', + 'build': '6f6057dc645fdb7547689183b2311063bd653ddf' }; /* @@ -207,7 +207,8 @@ provides: [Element.Delegation] ... */ -(function(){ + +(function(addEvent, removeEvent){ var match = /(.*?):relay\(([^)]+)\)$/, combinators = /[+>~\s]/, @@ -234,9 +235,6 @@ provides: [Element.Delegation] return null; }; - var oldAddEvent = Element.prototype.addEvent, - oldRemoveEvent = Element.prototype.removeEvent; - Element.implement({ addEvent: function(type, fn){ @@ -249,10 +247,10 @@ provides: [Element.Delegation] if (el) this.fireEvent(type, [e, el], 0, el); }.bind(this); monitors[type] = monitor; - oldAddEvent.call(this, splitted.event, monitor); + addEvent.call(this, splitted.event, monitor); } } - return oldAddEvent.apply(this, arguments); + return addEvent.apply(this, arguments); }, removeEvent: function(type, fn){ @@ -261,19 +259,18 @@ provides: [Element.Delegation] var events = this.retrieve('events'); if (!events || !events[type] || (fn && !events[type].keys.contains(fn))) return this; - if (fn) oldRemoveEvent.apply(this, [type, fn]); - else oldRemoveEvent.apply(this, type); + if (fn) removeEvent.apply(this, [type, fn]); + else removeEvent.apply(this, type); events = this.retrieve('events'); - if (events && events[type] && events[type].length == 0){ + if (events && events[type] && events[type].keys.length == 0){ var monitors = this.retrieve('$moo:delegateMonitors', {}); - oldRemoveEvent.apply(this, [splitted.event, monitors[type]]); + removeEvent.apply(this, [splitted.event, monitors[type]]); delete monitors[type]; } return this; } - - return oldRemoveEvent.apply(this, arguments); + return removeEvent.apply(this, arguments); }, fireEvent: function(type, args, delay, bind){ @@ -287,7 +284,7 @@ provides: [Element.Delegation] }); -})(); +})(Element.prototype.addEvent, Element.prototype.removeEvent); /* --- @@ -373,7 +370,7 @@ provides: [Fx.Accordion] ... */ -var Accordion = Fx.Accordion = new Class({ +Fx.Accordion = new Class({ Extends: Fx.Elements, @@ -395,10 +392,14 @@ var Accordion = Fx.Accordion = new Class({ }, initialize: function(){ - var params = Array.link(arguments, {'container': Element.type, 'options': Object.type, 'togglers': $defined, 'elements': $defined}); + var params = Array.link(arguments, { + 'container': Element.type, //deprecated + 'options': Object.type, + 'togglers': $defined, + 'elements': $defined + }); this.parent(params.elements, params.options); this.togglers = $$(params.togglers); - this.container = document.id(params.container); this.previous = -1; this.internalChain = new Chain(); if (this.options.alwaysHide) this.options.wait = true; @@ -422,7 +423,8 @@ var Accordion = Fx.Accordion = new Class({ for (var fx in this.effects) el.setStyle(fx, 0); } }, this); - if ($chk(this.options.display)) this.display(this.options.display, this.options.initialDisplayFx); + if ($chk(this.options.display) || this.options.initialDisplayFx === false) this.display(this.options.display, this.options.initialDisplayFx); + if (this.options.fixedHeight !== false) this.options.returnHeightToAuto = false; this.addEvent('complete', this.internalChain.callChain.bind(this.internalChain)); }, @@ -492,6 +494,37 @@ var Accordion = Fx.Accordion = new Class({ }); +/* + Compatibility with 1.2.0 +*/ +var Accordion = new Class({ + + Extends: Fx.Accordion, + + initialize: function(){ + this.parent.apply(this, arguments); + var params = Array.link(arguments, {'container': Element.type}); + this.container = params.container; + }, + + addSection: function(toggler, element, pos){ + toggler = document.id(toggler); + element = document.id(element); + var test = this.togglers.contains(toggler); + var len = this.togglers.length; + if (len && (!test || pos)){ + pos = $pick(pos, len - 1); + toggler.inject(this.togglers[pos], 'before'); + element.inject(toggler, 'after'); + } else if (this.container && !test){ + toggler.inject(this.container); + element.inject(this.container); + } + return this.parent.apply(this, arguments); + } + +}); + /* --- @@ -519,12 +552,14 @@ Fx.Slide = new Class({ options: { mode: 'vertical', + wrapper: false, hideOverflow: true }, initialize: function(element, options){ this.addEvent('complete', function(){ this.open = (this.wrapper['offset' + this.layout.capitalize()] != 0); + if (this.open) this.wrapper.setStyle('height', ''); if (this.open && Browser.Engine.webkit419) this.element.dispose().inject(this.wrapper); }, true); this.element = this.subject = document.id(element); @@ -532,6 +567,7 @@ Fx.Slide = new Class({ var wrapper = this.element.retrieve('wrapper'); var styles = this.element.getStyles('margin', 'position', 'overflow'); if (this.options.hideOverflow) styles = $extend(styles, {overflow: 'hidden'}); + if (this.options.wrapper) wrapper = document.id(this.options.wrapper).setStyles(styles); this.wrapper = wrapper || new Element('div', { styles: styles }).wraps(this.element); @@ -741,7 +777,8 @@ var Drag = new Class({ if (this.options.style) this.value.now[z] = this.element.getStyle(this.options.modifiers[z]).toInt(); else this.value.now[z] = this.element[this.options.modifiers[z]]; if (this.options.invert) this.value.now[z] *= -1; - this.mouse.pos[z] = event.page[z] - this.value.now[z]; + /*this.mouse.pos[z] = event.page[z] - this.value.now[z];*/ + this.mouse.pos[z] = this.value.now[z]; if (limit && limit[z]){ for (var i = 2; i--; i){ if ($chk(limit[z][i])) this.limit[z][i] = $lambda(limit[z][i])(); @@ -871,7 +908,7 @@ Drag.Move = new Class({ if (this.container && $type(this.container) != 'element') this.container = document.id(this.container.getDocument().body); - var styles = element.getStyles('left', 'right', 'position'); + var styles = element.getStyles('left', 'top', 'position'); if (styles.left == 'auto' || styles.top == 'auto') element.setPosition(element.getPosition(element.getOffsetParent())); @@ -1096,12 +1133,20 @@ var Sortables = new Class({ getClone: function(event, element){ if (!this.options.clone) return new Element('div').inject(document.body); if ($type(this.options.clone) == 'function') return this.options.clone.call(this, event, element, this.list); - return element.clone(true).setStyles({ + var clone = element.clone(true).setStyles({ margin: '0px', position: 'absolute', visibility: 'hidden', 'width': element.getStyle('width') - }).inject(this.list).setPosition(element.getPosition(element.getOffsetParent())); + }); + //prevent the duplicated radio inputs from unchecking the real one + if (clone.get('html').test('radio')) { + clone.getElements('input[type=radio]').each(function(input, i) { + input.set('name', 'clone_' + i); + }); + } + + return clone.inject(this.list).setPosition(element.getPosition(element.getOffsetParent())); }, getDroppables: function(){ @@ -1269,34 +1314,38 @@ var HtmlTable = new Class({ return this; }, + set: function(what, items) { + var target = (what == 'headers') ? 'tHead' : 'tFoot'; + this[target.toLowerCase()] = (document.id(this.element[target]) || new Element(target.toLowerCase()).inject(this.element, 'top')).empty(); + var data = this.push(items, {}, this[target.toLowerCase()], what == 'headers' ? 'th' : 'td'); + if (what == 'headers') this.head = document.id(this.thead.rows[0]); + else this.foot = document.id(this.thead.rows[0]); + return data; + }, + setHeaders: function(headers){ - this.thead = (document.id(this.element.tHead) || new Element('thead').inject(this.element, 'top')).empty(); - this.push(headers, this.thead, 'th'); - this.head = document.id(this.thead.rows[0]); + this.set('headers', headers); return this; }, setFooters: function(footers){ - this.tfoot = (document.id(this.element.tFoot) || new Element('tfoot').inject(this.element, 'top')).empty(); - this.push(footers, this.tfoot); - this.foot = document.id(this.thead.rows[0]); + this.set('footers', footers); return this; }, - push: function(row, target, tag){ + push: function(row, rowProperties, target, tag){ var tds = row.map(function(data){ var td = new Element(tag || 'td', data.properties), type = data.content || data || '', element = document.id(type); - - if(element) td.adopt(element); + if($type(type) != 'string' && element) td.adopt(element); else td.set('html', type); return td; }); return { - tr: new Element('tr').inject(target || this.body).adopt(tds), + tr: new Element('tr', rowProperties).inject(target || this.body).adopt(tds), tds: tds }; } @@ -1331,8 +1380,8 @@ provides: [HtmlTable.Select] HtmlTable = Class.refactor(HtmlTable, { options: { - /*onRowSelect: $empty, - onRowUnselect: $empty,*/ + /*onRowFocus: $empty, + onRowUnfocus: $empty,*/ useKeyboard: true, classRowSelected: 'table-tr-selected', classRowHovered: 'table-tr-hovered', @@ -1481,6 +1530,7 @@ HtmlTable = Class.refactor(HtmlTable, { }); + /* --- @@ -1507,37 +1557,8 @@ provides: [Keyboard] */ (function(){ - - var parsed = {}; - var modifiers = ['shift', 'control', 'alt', 'meta']; - var regex = /^(?:shift|control|ctrl|alt|meta)$/; - var parse = function(type, eventType){ - type = type.toLowerCase().replace(/^(keyup|keydown):/, function($0, $1){ - eventType = $1; - return ''; - }); - - if (!parsed[type]){ - var key = '', mods = {}; - type.split('+').each(function(part){ - if (regex.test(part)) mods[part] = true; - else key = part; - }); - - mods.control = mods.control || mods.ctrl; // allow both control and ctrl - var match = ''; - modifiers.each(function(mod){ - if (mods[mod]) match += mod + '+'; - }); - - parsed[type] = match + key; - } - - return eventType + ':' + parsed[type]; - }; - - this.Keyboard = new Class({ + var Keyboard = this.Keyboard = new Class({ Extends: Events, @@ -1550,24 +1571,24 @@ provides: [Keyboard] */ defaultEventType: 'keydown', active: false, - events: {} + events: {}, + nonParsedEvents: ['activate', 'deactivate', 'onactivate', 'ondeactivate', 'changed', 'onchanged'] }, initialize: function(options){ this.setOptions(options); - //if this is the root manager, nothing manages it - if (Keyboard.manager) Keyboard.manager.manage(this); this.setup(); - }, - + }, setup: function(){ this.addEvents(this.options.events); + //if this is the root manager, nothing manages it + if (Keyboard.manager && !this.manager) Keyboard.manager.manage(this); if (this.options.active) this.activate(); }, handle: function(event, type){ //Keyboard.stop(event) prevents key propagation - if (!this.active || event.preventKeyboardPropagation) return; + if (event.preventKeyboardPropagation) return; var bubbles = !!this.manager; if (bubbles && this.activeKB){ @@ -1579,59 +1600,64 @@ provides: [Keyboard] if (!bubbles && this.activeKB) this.activeKB.handle(event, type); }, - addEvent: function(type, fn, internal) { - return this.parent(parse(type, this.options.defaultEventType), fn, internal); + addEvent: function(type, fn, internal){ + return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn, internal); }, - removeEvent: function(type, fn) { - return this.parent(parse(type, this.options.defaultEventType), fn); - }, - - activate: function(){ - this.active = true; - return this.enable(); - }, - - deactivate: function(){ - this.active = false; - return this.fireEvent('deactivate'); + removeEvent: function(type, fn){ + return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn); }, toggleActive: function(){ return this[this.active ? 'deactivate' : 'activate'](); }, - enable: function(instance){ + activate: function(instance){ if (instance) { //if we're stealing focus, store the last keyboard to have it so the relenquish command works if (instance != this.activeKB) this.previous = this.activeKB; //if we're enabling a child, assign it so that events are now passed to it this.activeKB = instance.fireEvent('activate'); + Keyboard.manager.fireEvent('changed'); } else if (this.manager) { //else we're enabling ourselves, we must ask our parent to do it for us - this.manager.enable(this); + this.manager.activate(this); + } + return this; + }, + + deactivate: function(instance){ + if (instance) { + if(instance === this.activeKB) { + this.activeKB = null; + instance.fireEvent('deactivate'); + Keyboard.manager.fireEvent('changed'); + } + } + else if (this.manager) { + this.manager.deactivate(this); } return this; }, relenquish: function(){ - if (this.previous) this.enable(this.previous); + if (this.previous) this.activate(this.previous); }, //management logic - manage: function(instance) { + manage: function(instance){ if (instance.manager) instance.manager.drop(instance); this.instances.push(instance); instance.manager = this; - if (!this.activeKB) this.enable(instance); + if (!this.activeKB) this.activate(instance); else this._disable(instance); }, - _disable: function(instance) { + _disable: function(instance){ if (this.activeKB == instance) this.activeKB = null; }, - drop: function(instance) { + drop: function(instance){ this._disable(instance); this.instances.erase(instance); }, @@ -1639,35 +1665,81 @@ provides: [Keyboard] instances: [], trace: function(){ - this.enableLog(); - var item = this; - this.log('the following items have focus: '); - while (item) { - this.log(document.id(item.widget) || item.widget || item, 'active: ' + this.active); - item = item.activeKB; - } + Keyboard.trace(this); + }, + + each: function(fn){ + Keyboard.each(this, fn); } }); + + var parsed = {}; + var modifiers = ['shift', 'control', 'alt', 'meta']; + var regex = /^(?:shift|control|ctrl|alt|meta)$/; + + Keyboard.parse = function(type, eventType, ignore){ + if (ignore && ignore.contains(type.toLowerCase())) return type; + + type = type.toLowerCase().replace(/^(keyup|keydown):/, function($0, $1){ + eventType = $1; + return ''; + }); - Keyboard.stop = function(event) { + if (!parsed[type]){ + var key, mods = {}; + type.split('+').each(function(part){ + if (regex.test(part)) mods[part] = true; + else key = part; + }); + + mods.control = mods.control || mods.ctrl; // allow both control and ctrl + + var keys = []; + modifiers.each(function(mod){ + if (mods[mod]) keys.push(mod); + }); + + if (key) keys.push(key); + parsed[type] = keys.join('+'); + } + + return eventType + ':' + parsed[type]; + }; + + Keyboard.each = function(keyboard, fn){ + var current = keyboard || Keyboard.manager; + while (current){ + fn.run(current); + current = current.activeKB; + } + }; + + Keyboard.stop = function(event){ event.preventKeyboardPropagation = true; }; - Keyboard.manager = new this.Keyboard({ + Keyboard.manager = new Keyboard({ active: true }); - Keyboard.trace = function(){ - Keyboard.manager.trace(); + Keyboard.trace = function(keyboard){ + keyboard = keyboard || Keyboard.manager; + keyboard.enableLog(); + keyboard.log('the following items have focus: '); + Keyboard.each(keyboard, function(current){ + keyboard.log(document.id(current.widget) || current.wiget || current); + }); }; var handler = function(event){ - var mods = ''; + var keys = []; modifiers.each(function(mod){ - if (event[mod]) mods += mod + '+'; + if (event[mod]) keys.push(mod); }); - Keyboard.manager.handle(event, event.type + ':' + mods + event.key); + + if (!regex.test(event.key)) keys.push(event.key); + Keyboard.manager.handle(event, event.type + ':' + keys.join('+')); }; document.addEvents({ @@ -1676,13 +1748,27 @@ provides: [Keyboard] }); Event.Keys.extend({ + 'shift': 16, + 'control': 17, + 'alt': 18, + 'capslock': 20, 'pageup': 33, 'pagedown': 34, 'end': 35, 'home': 36, - 'capslock': 20, 'numlock': 144, - 'scrolllock': 145 + 'scrolllock': 145, + ';': 186, + '=': 187, + ',': 188, + '-': Browser.Engine.Gecko ? 109 : 189, + '.': 190, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + "'": 222 }); })(); @@ -1727,7 +1813,8 @@ var Scroller = new Class({ initialize: function(element, options){ this.setOptions(options); this.element = document.id(element); - this.listener = ($type(this.element) != 'element') ? document.id(this.element.getDocument().body) : this.element; + this.docBody = document.id(this.element.getDocument().body); + this.listener = ($type(this.element) != 'element') ? this.docBody : this.element; this.timer = null; this.bound = { attach: this.attach.bind(this), @@ -1769,14 +1856,15 @@ var Scroller = new Class({ scroll: function(){ var size = this.element.getSize(), scroll = this.element.getScroll(), - pos = this.element.getOffsets(), + pos = this.element != this.docBody ? this.element.getOffsets() : {x: 0, y:0}, scrollSize = this.element.getScrollSize(), change = {x: 0, y: 0}; for (var z in this.page){ - if (this.page[z] < (this.options.area + pos[z]) && scroll[z] != 0) + if (this.page[z] < (this.options.area + pos[z]) && scroll[z] != 0) { change[z] = (this.page[z] - this.options.area - pos[z]) * this.options.velocity; - else if (this.page[z] + this.options.area > (size[z] + pos[z]) && scroll[z] + size[z] != scrollSize[z]) + } else if (this.page[z] + this.options.area > (size[z] + pos[z]) && scroll[z] + size[z] != scrollSize[z]) { change[z] = (this.page[z] - size[z] + this.options.area - pos[z]) * this.options.velocity; + } } if (change.y || change.x) this.fireEvent('change', [scroll.x + change.x, scroll.y + change.y]); } @@ -1838,21 +1926,20 @@ this.Tips = new Class({ hideDelay: 100, className: 'tip-wrap', offset: {x: 16, y: 16}, + windowPadding: {x:0, y:0}, fixed: false }, initialize: function(){ var params = Array.link(arguments, {options: Object.type, elements: $defined}); this.setOptions(params.options); - document.id(this); - if (params.elements) this.attach(params.elements); + this.container = new Element('div', {'class': 'tip'}); }, toElement: function(){ if (this.tip) return this.tip; - - this.container = new Element('div', {'class': 'tip'}); + return this.tip = new Element('div', { 'class': this.options.className, styles: { @@ -1916,8 +2003,10 @@ this.Tips = new Class({ }, this); $clear(this.timer); - this.timer = this.show.delay(this.options.showDelay, this, element); - this.position((this.options.fixed) ? {page: element.getPosition()} : event); + this.timer = (function(){ + this.show(this, element); + this.position((this.options.fixed) ? {page: element.getPosition()} : event); + }).delay(this.options.showDelay, this); }, elementLeave: function(event, element){ @@ -1927,11 +2016,10 @@ this.Tips = new Class({ }, fireForParent: function(event, element){ - if (!element) return; - parentNode = element.getParent(); - if (parentNode == document.body) return; - if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event); - else this.fireForParent(parentNode, event); + element = element.getParent(); + if (!element || element == document.body) return; + if (element.retrieve('tip:enter')) element.fireEvent('mouseenter', event); + else this.fireForParent(event, element); }, elementMove: function(event, element){ @@ -1939,6 +2027,8 @@ this.Tips = new Class({ }, position: function(event){ + if (!this.tip) document.id(this); + var size = window.getSize(), scroll = window.getScroll(), tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, props = {x: 'left', y: 'top'}, @@ -1946,7 +2036,7 @@ this.Tips = new Class({ for (var z in props){ obj[props[z]] = event.page[z] + this.options.offset[z]; - if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; + if ((obj[props[z]] + tip[z] - scroll[z]) > size[z] - this.options.windowPadding[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; } this.tip.setStyles(obj); @@ -1958,10 +2048,12 @@ this.Tips = new Class({ }, show: function(element){ + if (!this.tip) document.id(this); this.fireEvent('show', [this.tip, element]); }, hide: function(element){ + if (!this.tip) document.id(this); this.fireEvent('hide', [this.tip, element]); } diff --git a/static/stylesheets/default.css b/static/stylesheets/default.css index 008f1f96e..9e8e6e7a2 100644 --- a/static/stylesheets/default.css +++ b/static/stylesheets/default.css @@ -169,6 +169,14 @@ div#footer { color: white; } +.menubardropdown ul { + background: white; + width: 120px; + border-bottom: 5px solid #ccc; + border-right: 5px solid #ccc; + border-left: 5px solid #ccc; +} + .left { float: left; } @@ -185,20 +193,32 @@ div#footer { .dropdown { position: absolute; + clear: both; + z-index: 2; + width: 250px; +} + +.dropdown li { + padding: 5px; + width: 170px; + cursor: pointer; } .dropdown ul { - display: block; - background: #ffffcc; - width: 150px; - height: 90px; - margin: 0px; - padding: 0px; - padding-top: 10px; + list-style-type: none; } -.dropdown a, .dropdown a:visited { - color: black !important; +.topdropdown ul { + background: #153F65; + border-bottom: 5px solid #333; + border-right: 5px solid #333; + border-left: 5px solid #333; + width: 150px; +} + +.dropdown ul li { + display: block; + float: none; } .spacer { @@ -248,17 +268,17 @@ th, td { height: 20px; } -table th { +th { background: #CEDBEC; text-align: left; padding: 5px; } -table th.right { +th.right { text-align: right; } -table td { +td { border-bottom: 1px solid #C8D6E8; /*padding: 5px;*/ } diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html index 959388161..e0c19ef14 100644 --- a/templates/common/email_listing.html +++ b/templates/common/email_listing.html @@ -17,22 +17,19 @@ html > body #listing { } #navbar { + float: right; padding: 0; margin: 0; - background: white; - height: 40px; - width: 100%; text-align: right; - /*border-top: 1px solid #C8D6E8;*/ - display: none; } #navbar label { + color: white; margin-right: 5px; font-weight: bold; } -#navbar img { +#navbar img{ margin-top: 5px; margin-right: 5px; } @@ -57,5 +54,6 @@ window.addEvent("domready", function() { {% block leftcol %}{% endblock %}
{{ listing }}
- {% endblock %} +{% block footer %}{% endblock %} + diff --git a/templates/common/table_head.html b/templates/common/table_head.html index 79bc8f15c..9c70f8c89 100644 --- a/templates/common/table_head.html +++ b/templates/common/table_head.html @@ -1,9 +1,13 @@ {% for c in table.columns %} + {% if c.sortable %} + {% else %} + + {% endif %} {{ c|safe }} - + {% endfor %} diff --git a/templates/main/menu.html b/templates/main/menu.html index 86111face..1de27a857 100644 --- a/templates/main/menu.html +++ b/templates/main/menu.html @@ -2,14 +2,14 @@