diff --git a/.hgignore b/.hgignore
index 9fd5fab25..06861c4f7 100644
--- a/.hgignore
+++ b/.hgignore
@@ -1,3 +1,4 @@
.*~$
^[^\.]+\.pyc
static/tmp
+static/ckeditor
diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py
index b5a2d33ce..eae4295b9 100644
--- a/extensions/webmail/imap_listing.py
+++ b/extensions/webmail/imap_listing.py
@@ -13,12 +13,14 @@ from mailng.lib import tables, imap_utf7, parameters
class WMtable(tables.Table):
+ tableid = "emails"
idkey = "imapid"
- selection = tables.SelectionColumn("selection", first=True)
- subject = tables.Column("subject", label=_("Subject"))
- from_ = tables.Column("from", label=_("From"))
- date = tables.Column("date", label=_("Date"))
- flags = tables.ImgColumn("flags")
+ selection = tables.SelectionColumn("selection", width="20px", first=True)
+ _1_flags = tables.ImgColumn("flags", width="40px")
+ _2_subject = tables.Column("subject", width="50%", label=_("Subject"))
+ _3_from_ = tables.Column("from", width="20%", label=_("From"))
+ _4_date = tables.Column("date", width="20%", label=_("Date"))
+
def parse(self, header, value):
try:
diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py
index a3e7b18e6..c02b0c938 100644
--- a/extensions/webmail/main.py
+++ b/extensions/webmail/main.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import time
+import sys
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson
@@ -33,41 +34,46 @@ def menu(**kwargs):
return []
return [
{"name" : _("Webmail"),
- "url" : reverse(folder),
+ "url" : reverse(index),
"img" : "/static/pics/webmail.png"}
]
def userlogin(**kwargs):
kwargs["request"].session["password"] = kwargs["password"]
-def folder_ajax(request, name, pageid, menu=False):
+def getctx(status, **kwargs):
+ callername = sys._getframe(1).f_code.co_name
+ ctx = {"status" : status, "callback" : callername}
+ for kw, v in kwargs.iteritems():
+ ctx[kw] = v
+ return ctx
+
+@login_required
+def folder(request, name, updatenav=True):
+ if not name:
+ name = "INBOX"
+ if updatenav:
+ pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1
+ request.session["folder"] = name
+ request.session["page"] = pageid
lst = ImapListing(request.user.username, request.session["password"],
- reverse(folder, args=[name]), folder=name)
- page = lst.paginator.getpage(pageid)
+ name, folder=name)
+ page = lst.paginator.getpage(request.session["page"])
if page:
content = lst.fetch(request, page.id_start, page.id_stop)
navbar = lst.render_navbar(page)
else:
content = "Empty folder"
navbar = ""
- ctx = {"status" : "ok", "listing" : content, "navbar" : navbar}
- if menu or "menu" in request.GET.keys():
- from templatetags.webextras import listing_menu
- ctx["menu"] = listing_menu("", request.user.get_all_permissions())
+ ctx = getctx("ok", listing=content, navbar=navbar,
+ menu=listing_menu("", request.user.get_all_permissions()))
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
-def folder(request, name=None):
- if not name:
- name = "INBOX"
- pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1
- request.session["folder"] = name
- request.session["page"] = pageid
- if "mode" in request.GET.keys() and request.GET["mode"] == "ajax":
- return folder_ajax(request, name, pageid)
+def index(request):
lst = ImapListing(request.user.username, request.session["password"],
- reverse(folder, args=[name]), folder=name)
- return lst.render(request, pageid=int(pageid))
+ "INBOX", folder="INBOX")
+ return lst.render(request, empty=True)
def fetchmail(request, folder, mail_id, all=False):
global mbc
@@ -84,14 +90,14 @@ def viewmail(request, folder, mail_id=None):
header = fetchmail(request, folder, mail_id)
if header:
from templatetags.webextras import viewm_menu
- menu = viewm_menu("", request.session["folder"], mail_id,
- request.session["page"],
+ pageid = request.session.has_key("page") and request.session["page"] or "1"
+ menu = viewm_menu("", folder, mail_id, pageid,
request.user.get_all_permissions())
folder, imapid = header["imapid"].split("/")
mailcontent = render_to_string("webmail/viewmail.html", {
"header" : header, "folder" : folder, "imapid" : imapid
})
- ctx = {"status" : "ok", "menu" : menu, "listing" : mailcontent}
+ ctx = getctx("ok", menu=menu, listing=mailcontent)
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
@@ -122,8 +128,7 @@ def compose(request):
s = smtplib.SMTP(parameters.get("webmail", "SMTP_SERVER"))
s.sendmail(msg['From'], [msg['To']], msg.as_string())
s.quit()
- return folder_ajax(request, request.session["folder"],
- request.session["page"], True)
+ return folder(request, request.session["folder"], False)
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
@@ -132,5 +137,5 @@ def compose(request):
content = render_to_string("webmail/compose.html", {
"form" : form
})
- ctx = {"status" : "ok", "menu" : menu, "listing" : content}
+ ctx = getctx("ok", menu=menu, listing=content)
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html
index 0a9c16af8..d44d1aca1 100644
--- a/extensions/webmail/templates/index.html
+++ b/extensions/webmail/templates/index.html
@@ -44,9 +44,16 @@
margin-right: 2px;
border: 0;
}
+
+tr.table-tr-selected {
+ background: red;
+}
{% endblock %}
{% block js %}
updatelisting = function(resp, overflow) {
+ if (!$defined(overflow)) {
+ overflow = "auto";
+ }
if ($defined(resp.menu)) {
$("menubar").set("html", resp.menu);
}
@@ -60,46 +67,68 @@
}
{% url extensions.webmail.main.folder as view_url %}
- loadMail = function(event) {
+ viewmail = function(event) {
event.stop();
if (event.target.parentNode.id) {
- var url = "{{ view_url }}" + event.target.parentNode.id;
+ var url = event.target.parentNode.id + "/";
event.target.parentNode.setStyle("background", "#ffffcc");
- new Request.JSON({url: url, onSuccess: function(resp) {
- updatelisting(resp, "hidden");
- $$("a[name=back]").addEvent("click", loadFolder);
- setDivHeight("mailcontent", $("table-container").getSize().y, 0);
- }}).get();
+ location.hash = url;
}
}
+ register_callback("viewmail", function(resp) {
+ updatelisting(resp, "hidden");
+ $$("a[name=back]").addEvent("click", loadFolder);
+ setDivHeight("mailcontent", $("table-container").getSize().y, 0);
+ });
loadCompose = function(event) {
event.stop();
- new Request.JSON({url: event.target, onSuccess: function(resp) {
- updatelisting(resp, "hidden");
- $$("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", loadMail);
- }
- });
- $("composemail").send();
- });
- setDivHeight("id_body", $("mailheader").getSize().y + 20, 0);
- }}).get();
+ location.hash = "compose/";
}
+ register_callback("compose", function(resp) {
+ updatelisting(resp, "hidden");
+ $$("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);
+ }
+ });
+ $("composemail").send();
+ });
+ setDivHeight("id_body", $("mailheader").getSize().y + 20, 0);
+ });
loadFolder = function(event, obj) {
- obj = typeof(obj) != "undefined" ? obj : event.target;
+ if (!$defined(obj)) {
+ obj = event.target;
+ }
event.stop();
- new Request.JSON({url: obj.get("href"), onSuccess: function(resp) {
- updatelisting(resp, "auto");
- $$("tr").addEvent("click", loadMail);
- }}).get({"mode" : "ajax"});
+ location.hash = obj.get("href");
}
+ register_callback("folder", function(resp) {
+ updatelisting(resp, "auto");
+ var tbl = new HtmlTable($("emails"), {
+ selectable: true
+ });
+ var drag_group = new Drag.Group({
+ });
+ tbl.addEvent("rowFocus", function(obj) {
+ var clone = obj.clone()
+ .setStyles(obj.getCoordinates())
+ .setStyles({"opacity": 0.7, "position": "absolute"})
+ .inject(document.body);
+ drag_group.add(clone, {
+ onComplete: function(item) {
+ item.dispose();
+ delete(item);
+ }
+ });
+ });
+ $$("tr").addEvent("dblclick", viewmail);
+ });
navbarcallback = function(event) {
loadFolder(event, event.target.getParent());
@@ -117,9 +146,9 @@
loadFolder(event, event.target);
}
+ ajaxListener(updatelisting);
$$("a[name=loadfolder]").addEvent("click", foldercallback);
$("navbar").getChildren("a").addEvent("click", navbarcallback);
- $$("tr").addEvent("click", loadMail);
$$("a[name=compose]").addEvent("click", loadCompose);
{% endblock %}
{% block middletitle %}{% trans "Webmail" %}{% endblock %}
@@ -133,7 +162,7 @@
{% for fd in folders %}
{% url extensions.webmail.main.folder fd.name as fdurl %}
-
+
-
{% if fd.icon %}
{% else %}
diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py
index 6e9c86a9d..c6f2b6e0f 100644
--- a/extensions/webmail/templatetags/webextras.py
+++ b/extensions/webmail/templatetags/webextras.py
@@ -11,8 +11,7 @@ register = template.Library()
def viewm_menu(selection, folder, mail_id, page_id, perms):
entries = [
{"name" : "back",
- "url" : reverse(webmail.main.folder, args=[folder]) \
- + "?page=%s" % page_id + "&menu=1",
+ "url" : folder + "?page=%s" % page_id + "&menu=1",
"img" : "/static/pics/back.png",
"label" : _("Back to list")},
]
@@ -25,8 +24,7 @@ def viewm_menu(selection, folder, mail_id, page_id, perms):
def compose_menu(selection, folder, page_id, perms):
entries = [
{"name" : "back",
- "url" : reverse(webmail.main.folder, args=[folder]) \
- + "?page=%s" % page_id + "&menu=1",
+ "url" : folder + "?page=%s" % page_id + "&menu=1",
"img" : "/static/pics/back.png",
"label" : _("Back to list")},
{"name" : "sendmail",
diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py
index 5dc10bfb4..5f155aa52 100644
--- a/extensions/webmail/urls.py
+++ b/extensions/webmail/urls.py
@@ -1,9 +1,9 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('mailng.extensions.webmail.main',
- url(r'^$', "folder", name="index"),
+ (r'^$', "index"),
(r'^compose/$', "compose"),
(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'),
(r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'),
- url(r'^(?P.+)/$', "folder", name="folder"),
+ (r'^(?P.+)/$', "folder"),
)
diff --git a/lib/email_listing.py b/lib/email_listing.py
index 8b8f96352..9f2cbb875 100644
--- a/lib/email_listing.py
+++ b/lib/email_listing.py
@@ -102,10 +102,13 @@ class EmailListing:
def render(self, request, pageid=1, **kwargs):
page = self.paginator.getpage(pageid)
- if not page:
- listing = "Empty folder"
+ if "empty" in kwargs.keys() and not kwargs["empty"]:
+ if not page:
+ listing = "Empty folder"
+ else:
+ listing = self.fetch(request, page.id_start, page.id_stop)
else:
- listing = self.fetch(request, page.id_start, page.id_stop)
+ listing = ""
elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0
return _render(request, self.tpl, {
"listing" : listing, "elapsed" : elapsed,
diff --git a/lib/tables.py b/lib/tables.py
index 13a36eba1..4dfe23281 100644
--- a/lib/tables.py
+++ b/lib/tables.py
@@ -32,6 +32,8 @@ class ImgColumn(Column):
return "
" % value
class Table(object):
+ tableid = ""
+
def __init__(self, rows={}):
self.columns = []
for m in inspect.getmembers(self):
@@ -67,5 +69,5 @@ class Table(object):
def render(self, request):
return render_to_string("common/tables.html", {
- "table" : self
+ "table" : self, "tableid" : self.tableid
})
diff --git a/static/js/common.js b/static/js/common.js
index 9940cb67f..44c17ef94 100644
--- a/static/js/common.js
+++ b/static/js/common.js
@@ -6,6 +6,45 @@ window.addEvent('domready', function(){
});
});
+callbacks = {};
+
+register_callback = function(name, callback) {
+ callbacks[name] = callback;
+}
+
+current_anchor = null;
+
+check_anchor = function() {
+ if (current_anchor == document.location.hash) {
+ return;
+ }
+ current_anchor = document.location.hash;
+ if (!current_anchor) {
+ query = "INBOX/"; // Default location
+ } else {
+ var splits = current_anchor.substring(1).split('?');
+ var section = splits[0];
+ delete splits[0];
+ var params = splits.join('&');
+ if (section[section.length - 1] != '/') {
+ section += "/";
+ }
+ var query = section;
+ if (params != "") {
+ query += "?" + params;
+ }
+ }
+ new Request.JSON({url: query, onSuccess: function(resp) {
+ callback = ($defined(resp.callback)) ? resp.callback : "default";
+ callbacks[callback](resp);
+ }}).get();
+}
+
+ajaxListener = function(defcallback) {
+ register_callback("default", defcallback);
+ check_anchor.periodical(300);
+}
+
parse_menubar = function(id) {
if ($(id)) {
$(id).getElements('li.dropdown').each( function( elem ){
diff --git a/static/js/mootools-more.js b/static/js/mootools-more.js
index 6dce4406c..9658e2da6 100644
--- a/static/js/mootools-more.js
+++ b/static/js/mootools-more.js
@@ -30,6 +30,268 @@ MooTools.More = {
/*
---
+script: Log.js
+
+description: Provides basic logging functionality for plugins to implement.
+
+license: MIT-style license
+
+authors:
+- Guillermo Rauch
+- Thomas Aylott
+- Scott Kyle
+
+requires:
+- core:1.2.4/Class
+- /MooTools.More
+
+provides: [Log]
+
+...
+*/
+
+(function(){
+
+var global = this;
+
+var log = function(){
+ if (global.console && console.log){
+ try {
+ console.log.apply(console, arguments);
+ } catch(e) {
+ console.log(Array.slice(arguments));
+ }
+ } else {
+ Log.logged.push(arguments);
+ }
+ return this;
+};
+
+var disabled = function(){
+ this.logged.push(arguments);
+ return this;
+};
+
+this.Log = new Class({
+
+ logged: [],
+
+ log: disabled,
+
+ resetLog: function(){
+ this.logged.empty();
+ return this;
+ },
+
+ enableLog: function(){
+ this.log = log;
+ this.logged.each(function(args){
+ this.log.apply(this, args);
+ }, this);
+ return this.resetLog();
+ },
+
+ disableLog: function(){
+ this.log = disabled;
+ return this;
+ }
+
+});
+
+Log.extend(new Log).enableLog();
+
+// legacy
+Log.logger = function(){
+ return this.log.apply(this, arguments);
+};
+
+})();
+
+/*
+---
+
+script: Class.Refactor.js
+
+description: Extends a class onto itself with new property, preserving any items attached to the class's namespace.
+
+license: MIT-style license
+
+authors:
+- Aaron Newton
+
+requires:
+- core:1.2.4/Class
+- /MooTools.More
+
+provides: [Class.refactor]
+
+...
+*/
+
+Class.refactor = function(original, refactors){
+
+ $each(refactors, function(item, name){
+ var origin = original.prototype[name];
+ if (origin && (origin = origin._origin) && typeof item == 'function') original.implement(name, function(){
+ var old = this.previous;
+ this.previous = origin;
+ var value = item.apply(this, arguments);
+ this.previous = old;
+ return value;
+ }); else original.implement(name, item);
+ });
+
+ return original;
+
+};
+
+/*
+---
+
+script: Class.Occlude.js
+
+description: Prevents a class from being applied to a DOM element twice.
+
+license: MIT-style license.
+
+authors:
+- Aaron Newton
+
+requires:
+- core/1.2.4/Class
+- core:1.2.4/Element
+- /MooTools.More
+
+provides: [Class.Occlude]
+
+...
+*/
+
+Class.Occlude = new Class({
+
+ occlude: function(property, element){
+ element = document.id(element || this.element);
+ var instance = element.retrieve(property || this.property);
+ if (instance && !$defined(this.occluded))
+ return this.occluded = instance;
+
+ this.occluded = false;
+ element.store(property || this.property, this);
+ return this.occluded;
+ }
+
+});
+
+/*
+---
+
+script: Element.Delegation.js
+
+description: Extends the Element native object to include the delegate method for more efficient event management.
+
+credits:
+- "Event checking based on the work of Daniel Steigerwald. License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz"
+
+license: MIT-style license
+
+authors:
+- Aaron Newton
+- Daniel Steigerwald
+
+requires:
+- core:1.2.4/Element.Event
+- core:1.2.4/Selectors
+- /MooTools.More
+
+provides: [Element.Delegation]
+
+...
+*/
+(function(){
+
+ var match = /(.*?):relay\(([^)]+)\)$/,
+ combinators = /[+>~\s]/,
+ splitType = function(type){
+ var bits = type.match(match);
+ return !bits ? {event: type} : {
+ event: bits[1],
+ selector: bits[2]
+ };
+ },
+ check = function(e, selector){
+ var t = e.target;
+ if (combinators.test(selector = selector.trim())){
+ var els = this.getElements(selector);
+ for (var i = els.length; i--; ){
+ var el = els[i];
+ if (t == el || el.hasChild(t)) return el;
+ }
+ } else {
+ for ( ; t && t != this; t = t.parentNode){
+ if (Element.match(t, selector)) return document.id(t);
+ }
+ }
+ return null;
+ };
+
+ var oldAddEvent = Element.prototype.addEvent,
+ oldRemoveEvent = Element.prototype.removeEvent;
+
+ Element.implement({
+
+ addEvent: function(type, fn){
+ var splitted = splitType(type);
+ if (splitted.selector){
+ var monitors = this.retrieve('$moo:delegateMonitors', {});
+ if (!monitors[type]){
+ var monitor = function(e){
+ var el = check.call(this, e, splitted.selector);
+ if (el) this.fireEvent(type, [e, el], 0, el);
+ }.bind(this);
+ monitors[type] = monitor;
+ oldAddEvent.call(this, splitted.event, monitor);
+ }
+ }
+ return oldAddEvent.apply(this, arguments);
+ },
+
+ removeEvent: function(type, fn){
+ var splitted = splitType(type);
+ if (splitted.selector){
+ 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);
+
+ events = this.retrieve('events');
+ if (events && events[type] && events[type].length == 0){
+ var monitors = this.retrieve('$moo:delegateMonitors', {});
+ oldRemoveEvent.apply(this, [splitted.event, monitors[type]]);
+ delete monitors[type];
+ }
+ return this;
+ }
+
+ return oldRemoveEvent.apply(this, arguments);
+ },
+
+ fireEvent: function(type, args, delay, bind){
+ var events = this.retrieve('events');
+ if (!events || !events[type]) return this;
+ events[type].keys.each(function(fn){
+ fn.create({bind: bind || this, delay: delay, arguments: args})();
+ }, this);
+ return this;
+ }
+
+ });
+
+})();
+
+/*
+---
+
script: Fx.Slide.js
description: Effect to slide an element in and out of view.
@@ -182,6 +444,1047 @@ Element.implement({
});
+/*
+---
+
+script: Drag.js
+
+description: The base Drag Class. Can be used to drag and resize Elements using mouse events.
+
+license: MIT-style license
+
+authors:
+- Valerio Proietti
+- Tom Occhinno
+- Jan Kassens
+
+requires:
+- core:1.2.4/Events
+- core:1.2.4/Options
+- core:1.2.4/Element.Event
+- core:1.2.4/Element.Style
+- /MooTools.More
+
+provides: [Drag]
+
+*/
+
+var Drag = new Class({
+
+ Implements: [Events, Options],
+
+ options: {/*
+ onBeforeStart: $empty(thisElement),
+ onStart: $empty(thisElement, event),
+ onSnap: $empty(thisElement)
+ onDrag: $empty(thisElement, event),
+ onCancel: $empty(thisElement),
+ onComplete: $empty(thisElement, event),*/
+ snap: 6,
+ unit: 'px',
+ grid: false,
+ style: true,
+ limit: false,
+ handle: false,
+ invert: false,
+ preventDefault: false,
+ stopPropagation: false,
+ modifiers: {x: 'left', y: 'top'}
+ },
+
+ initialize: function(){
+ var params = Array.link(arguments, {'options': Object.type, 'element': $defined});
+ this.element = document.id(params.element);
+ this.document = this.element.getDocument();
+ this.setOptions(params.options || {});
+ var htype = $type(this.options.handle);
+ this.handles = ((htype == 'array' || htype == 'collection') ? $$(this.options.handle) : document.id(this.options.handle)) || this.element;
+ this.mouse = {'now': {}, 'pos': {}};
+ this.value = {'start': {}, 'now': {}};
+
+ this.selection = (Browser.Engine.trident) ? 'selectstart' : 'mousedown';
+
+ this.bound = {
+ start: this.start.bind(this),
+ check: this.check.bind(this),
+ drag: this.drag.bind(this),
+ stop: this.stop.bind(this),
+ cancel: this.cancel.bind(this),
+ eventStop: $lambda(false)
+ };
+ this.attach();
+ },
+
+ attach: function(){
+ this.handles.addEvent('mousedown', this.bound.start);
+ return this;
+ },
+
+ detach: function(){
+ this.handles.removeEvent('mousedown', this.bound.start);
+ return this;
+ },
+
+ start: function(event){
+ if (event.rightClick) return;
+ if (this.options.preventDefault) event.preventDefault();
+ if (this.options.stopPropagation) event.stopPropagation();
+ this.mouse.start = event.page;
+ this.fireEvent('beforeStart', this.element);
+ var limit = this.options.limit;
+ this.limit = {x: [], y: []};
+ for (var z in this.options.modifiers){
+ if (!this.options.modifiers[z]) continue;
+ 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];
+ if (limit && limit[z]){
+ for (var i = 2; i--; i){
+ if ($chk(limit[z][i])) this.limit[z][i] = $lambda(limit[z][i])();
+ }
+ }
+ }
+ if ($type(this.options.grid) == 'number') this.options.grid = {x: this.options.grid, y: this.options.grid};
+ this.document.addEvents({mousemove: this.bound.check, mouseup: this.bound.cancel});
+ this.document.addEvent(this.selection, this.bound.eventStop);
+ },
+
+ check: function(event){
+ if (this.options.preventDefault) event.preventDefault();
+ var distance = Math.round(Math.sqrt(Math.pow(event.page.x - this.mouse.start.x, 2) + Math.pow(event.page.y - this.mouse.start.y, 2)));
+ if (distance > this.options.snap){
+ this.cancel();
+ this.document.addEvents({
+ mousemove: this.bound.drag,
+ mouseup: this.bound.stop
+ });
+ this.fireEvent('start', [this.element, event]).fireEvent('snap', this.element);
+ }
+ },
+
+ drag: function(event){
+ if (this.options.preventDefault) event.preventDefault();
+ this.mouse.now = event.page;
+ for (var z in this.options.modifiers){
+ if (!this.options.modifiers[z]) continue;
+ this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
+ if (this.options.invert) this.value.now[z] *= -1;
+ if (this.options.limit && this.limit[z]){
+ if ($chk(this.limit[z][1]) && (this.value.now[z] > this.limit[z][1])){
+ this.value.now[z] = this.limit[z][1];
+ } else if ($chk(this.limit[z][0]) && (this.value.now[z] < this.limit[z][0])){
+ this.value.now[z] = this.limit[z][0];
+ }
+ }
+ if (this.options.grid[z]) this.value.now[z] -= ((this.value.now[z] - (this.limit[z][0]||0)) % this.options.grid[z]);
+ if (this.options.style) {
+ this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit);
+ } else {
+ this.element[this.options.modifiers[z]] = this.value.now[z];
+ }
+ }
+ this.fireEvent('drag', [this.element, event]);
+ },
+
+ cancel: function(event){
+ this.document.removeEvent('mousemove', this.bound.check);
+ this.document.removeEvent('mouseup', this.bound.cancel);
+ if (event){
+ this.document.removeEvent(this.selection, this.bound.eventStop);
+ this.fireEvent('cancel', this.element);
+ }
+ },
+
+ stop: function(event){
+ this.document.removeEvent(this.selection, this.bound.eventStop);
+ this.document.removeEvent('mousemove', this.bound.drag);
+ this.document.removeEvent('mouseup', this.bound.stop);
+ if (event) this.fireEvent('complete', [this.element, event]);
+ }
+
+});
+
+Element.implement({
+
+ makeResizable: function(options){
+ var drag = new Drag(this, $merge({modifiers: {x: 'width', y: 'height'}}, options));
+ this.store('resizer', drag);
+ return drag.addEvent('drag', function(){
+ this.fireEvent('resize', drag);
+ }.bind(this));
+ }
+
+});
+
+
+/*
+---
+
+script: Drag.Move.js
+
+description: A Drag extension that provides support for the constraining of draggables to containers and droppables.
+
+license: MIT-style license
+
+authors:
+- Valerio Proietti
+- Tom Occhinno
+- Jan Kassens
+- Aaron Newton
+- Scott Kyle
+
+requires:
+- core:1.2.4/Element.Dimensions
+- /Drag
+
+provides: [Drag.Move]
+
+...
+*/
+
+Drag.Move = new Class({
+
+ Extends: Drag,
+
+ options: {/*
+ onEnter: $empty(thisElement, overed),
+ onLeave: $empty(thisElement, overed),
+ onDrop: $empty(thisElement, overed, event),*/
+ droppables: [],
+ container: false,
+ precalculate: false,
+ includeMargins: true,
+ checkDroppables: true
+ },
+
+ initialize: function(element, options){
+ this.parent(element, options);
+ element = this.element;
+
+ this.droppables = $$(this.options.droppables);
+ this.container = document.id(this.options.container);
+
+ if (this.container && $type(this.container) != 'element')
+ this.container = document.id(this.container.getDocument().body);
+
+ var styles = element.getStyles('left', 'right', 'position');
+ if (styles.left == 'auto' || styles.top == 'auto')
+ element.setPosition(element.getPosition(element.getOffsetParent()));
+
+ if (styles.position == 'static')
+ element.setStyle('position', 'absolute');
+
+ this.addEvent('start', this.checkDroppables, true);
+
+ this.overed = null;
+ },
+
+ start: function(event){
+ if (this.container) this.options.limit = this.calculateLimit();
+
+ if (this.options.precalculate){
+ this.positions = this.droppables.map(function(el){
+ return el.getCoordinates();
+ });
+ }
+
+ this.parent(event);
+ },
+
+ calculateLimit: function(){
+ var offsetParent = this.element.getOffsetParent(),
+ containerCoordinates = this.container.getCoordinates(offsetParent),
+ containerBorder = {},
+ elementMargin = {},
+ elementBorder = {},
+ containerMargin = {},
+ offsetParentPadding = {};
+
+ ['top', 'right', 'bottom', 'left'].each(function(pad){
+ containerBorder[pad] = this.container.getStyle('border-' + pad).toInt();
+ elementBorder[pad] = this.element.getStyle('border-' + pad).toInt();
+ elementMargin[pad] = this.element.getStyle('margin-' + pad).toInt();
+ containerMargin[pad] = this.container.getStyle('margin-' + pad).toInt();
+ offsetParentPadding[pad] = offsetParent.getStyle('padding-' + pad).toInt();
+ }, this);
+
+ var width = this.element.offsetWidth + elementMargin.left + elementMargin.right,
+ height = this.element.offsetHeight + elementMargin.top + elementMargin.bottom,
+ left = 0,
+ top = 0,
+ right = containerCoordinates.right - containerBorder.right - width,
+ bottom = containerCoordinates.bottom - containerBorder.bottom - height;
+
+ if (this.options.includeMargins){
+ left += elementMargin.left;
+ top += elementMargin.top;
+ } else {
+ right += elementMargin.right;
+ bottom += elementMargin.bottom;
+ }
+
+ if (this.element.getStyle('position') == 'relative'){
+ var coords = this.element.getCoordinates(offsetParent);
+ coords.left -= this.element.getStyle('left').toInt();
+ coords.top -= this.element.getStyle('top').toInt();
+
+ left += containerBorder.left - coords.left;
+ top += containerBorder.top - coords.top;
+ right += elementMargin.left - coords.left;
+ bottom += elementMargin.top - coords.top;
+
+ if (this.container != offsetParent){
+ left += containerMargin.left + offsetParentPadding.left;
+ top += (Browser.Engine.trident4 ? 0 : containerMargin.top) + offsetParentPadding.top;
+ }
+ } else {
+ left -= elementMargin.left;
+ top -= elementMargin.top;
+
+ if (this.container == offsetParent){
+ right -= containerBorder.left;
+ bottom -= containerBorder.top;
+ } else {
+ left += containerCoordinates.left + containerBorder.left;
+ top += containerCoordinates.top + containerBorder.top;
+ }
+ }
+
+ return {
+ x: [left, right],
+ y: [top, bottom]
+ };
+ },
+
+ checkAgainst: function(el, i){
+ el = (this.positions) ? this.positions[i] : el.getCoordinates();
+ var now = this.mouse.now;
+ return (now.x > el.left && now.x < el.right && now.y < el.bottom && now.y > el.top);
+ },
+
+ checkDroppables: function(){
+ var overed = this.droppables.filter(this.checkAgainst, this).getLast();
+ if (this.overed != overed){
+ if (this.overed) this.fireEvent('leave', [this.element, this.overed]);
+ if (overed) this.fireEvent('enter', [this.element, overed]);
+ this.overed = overed;
+ }
+ },
+
+ drag: function(event){
+ this.parent(event);
+ if (this.options.checkDroppables && this.droppables.length) this.checkDroppables();
+ },
+
+ stop: function(event){
+ this.checkDroppables();
+ this.fireEvent('drop', [this.element, this.overed, event]);
+ this.overed = null;
+ return this.parent(event);
+ }
+
+});
+
+Element.implement({
+
+ makeDraggable: function(options){
+ var drag = new Drag.Move(this, options);
+ this.store('dragger', drag);
+ return drag;
+ }
+
+});
+
+
+/*
+---
+
+script: Sortables.js
+
+description: Class for creating a drag and drop sorting interface for lists of items.
+
+license: MIT-style license
+
+authors:
+- Tom Occhino
+
+requires:
+- /Drag.Move
+
+provides: [Slider]
+
+...
+*/
+
+var Sortables = new Class({
+
+ Implements: [Events, Options],
+
+ options: {/*
+ onSort: $empty(element, clone),
+ onStart: $empty(element, clone),
+ onComplete: $empty(element),*/
+ snap: 4,
+ opacity: 1,
+ clone: false,
+ revert: false,
+ handle: false,
+ constrain: false
+ },
+
+ initialize: function(lists, options){
+ this.setOptions(options);
+ this.elements = [];
+ this.lists = [];
+ this.idle = true;
+
+ this.addLists($$(document.id(lists) || lists));
+ if (!this.options.clone) this.options.revert = false;
+ if (this.options.revert) this.effect = new Fx.Morph(null, $merge({duration: 250, link: 'cancel'}, this.options.revert));
+ },
+
+ attach: function(){
+ this.addLists(this.lists);
+ return this;
+ },
+
+ detach: function(){
+ this.lists = this.removeLists(this.lists);
+ return this;
+ },
+
+ addItems: function(){
+ Array.flatten(arguments).each(function(element){
+ this.elements.push(element);
+ var start = element.retrieve('sortables:start', this.start.bindWithEvent(this, element));
+ (this.options.handle ? element.getElement(this.options.handle) || element : element).addEvent('mousedown', start);
+ }, this);
+ return this;
+ },
+
+ addLists: function(){
+ Array.flatten(arguments).each(function(list){
+ this.lists.push(list);
+ this.addItems(list.getChildren());
+ }, this);
+ return this;
+ },
+
+ removeItems: function(){
+ return $$(Array.flatten(arguments).map(function(element){
+ this.elements.erase(element);
+ var start = element.retrieve('sortables:start');
+ (this.options.handle ? element.getElement(this.options.handle) || element : element).removeEvent('mousedown', start);
+
+ return element;
+ }, this));
+ },
+
+ removeLists: function(){
+ return $$(Array.flatten(arguments).map(function(list){
+ this.lists.erase(list);
+ this.removeItems(list.getChildren());
+
+ return list;
+ }, this));
+ },
+
+ 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({
+ margin: '0px',
+ position: 'absolute',
+ visibility: 'hidden',
+ 'width': element.getStyle('width')
+ }).inject(this.list).setPosition(element.getPosition(element.getOffsetParent()));
+ },
+
+ getDroppables: function(){
+ var droppables = this.list.getChildren();
+ if (!this.options.constrain) droppables = this.lists.concat(droppables).erase(this.list);
+ return droppables.erase(this.clone).erase(this.element);
+ },
+
+ insert: function(dragging, element){
+ var where = 'inside';
+ if (this.lists.contains(element)){
+ this.list = element;
+ this.drag.droppables = this.getDroppables();
+ } else {
+ where = this.element.getAllPrevious().contains(element) ? 'before' : 'after';
+ }
+ this.element.inject(element, where);
+ this.fireEvent('sort', [this.element, this.clone]);
+ },
+
+ start: function(event, element){
+ if (!this.idle) return;
+ this.idle = false;
+ this.element = element;
+ this.opacity = element.get('opacity');
+ this.list = element.getParent();
+ this.clone = this.getClone(event, element);
+
+ this.drag = new Drag.Move(this.clone, {
+ snap: this.options.snap,
+ container: this.options.constrain && this.element.getParent(),
+ droppables: this.getDroppables(),
+ onSnap: function(){
+ event.stop();
+ this.clone.setStyle('visibility', 'visible');
+ this.element.set('opacity', this.options.opacity || 0);
+ this.fireEvent('start', [this.element, this.clone]);
+ }.bind(this),
+ onEnter: this.insert.bind(this),
+ onCancel: this.reset.bind(this),
+ onComplete: this.end.bind(this)
+ });
+
+ this.clone.inject(this.element, 'before');
+ this.drag.start(event);
+ },
+
+ end: function(){
+ this.drag.detach();
+ this.element.set('opacity', this.opacity);
+ if (this.effect){
+ var dim = this.element.getStyles('width', 'height');
+ var pos = this.clone.computePosition(this.element.getPosition(this.clone.offsetParent));
+ this.effect.element = this.clone;
+ this.effect.start({
+ top: pos.top,
+ left: pos.left,
+ width: dim.width,
+ height: dim.height,
+ opacity: 0.25
+ }).chain(this.reset.bind(this));
+ } else {
+ this.reset();
+ }
+ },
+
+ reset: function(){
+ this.idle = true;
+ this.clone.destroy();
+ this.fireEvent('complete', this.element);
+ },
+
+ serialize: function(){
+ var params = Array.link(arguments, {modifier: Function.type, index: $defined});
+ var serial = this.lists.map(function(list){
+ return list.getChildren().map(params.modifier || function(element){
+ return element.get('id');
+ }, this);
+ }, this);
+
+ var index = params.index;
+ if (this.lists.length == 1) index = 0;
+ return $chk(index) && index >= 0 && index < this.lists.length ? serial[index] : serial;
+ }
+
+});
+
+
+/*
+---
+
+script: HtmlTable.js
+
+description: Builds table elements with methods to add rows.
+
+license: MIT-style license
+
+authors:
+- Aaron Newton
+
+requires:
+- core:1.2.4/Options
+- core:1.2.4/Events
+- /Class.Occlude
+
+provides: [HtmlTable]
+
+...
+*/
+
+var HtmlTable = new Class({
+
+ Implements: [Options, Events, Class.Occlude],
+
+ options: {
+ properties: {
+ cellpadding: 0,
+ cellspacing: 0,
+ border: 0
+ },
+ rows: [],
+ headers: [],
+ footers: []
+ },
+
+ property: 'HtmlTable',
+
+ initialize: function(){
+ var params = Array.link(arguments, {options: Object.type, table: Element.type});
+ this.setOptions(params.options);
+ this.element = params.table || new Element('table', this.options.properties);
+ if (this.occlude()) return this.occluded;
+ this.build();
+ },
+
+ build: function(){
+ this.element.store('HtmlTable', this);
+
+ this.body = document.id(this.element.tBodies[0]) || new Element('tbody').inject(this.element);
+ $$(this.body.rows);
+
+ if (this.options.headers.length) this.setHeaders(this.options.headers);
+ else this.thead = document.id(this.element.tHead);
+ if (this.thead) this.head = document.id(this.thead.rows[0]);
+
+ if (this.options.footers.length) this.setFooters(this.options.footers);
+ this.tfoot = document.id(this.element.tFoot);
+ if (this.tfoot) this.foot = document.id(this.thead.rows[0]);
+
+ this.options.rows.each(function(row){
+ this.push(row);
+ }, this);
+
+ ['adopt', 'inject', 'wraps', 'grab', 'replaces', 'dispose'].each(function(method){
+ this[method] = this.element[method].bind(this.element);
+ }, this);
+ },
+
+ toElement: function(){
+ return this.element;
+ },
+
+ empty: function(){
+ this.body.empty();
+ return this;
+ },
+
+ 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]);
+ 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]);
+ return this;
+ },
+
+ push: function(row, 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);
+ else td.set('html', type);
+
+ return td;
+ });
+
+ return {
+ tr: new Element('tr').inject(target || this.body).adopt(tds),
+ tds: tds
+ };
+ }
+
+});
+
+
+/*
+---
+
+script: HtmlTable.Select.js
+
+description: Builds a stripy, sortable table with methods to add rows. Rows can be selected with the mouse or keyboard navigation.
+
+license: MIT-style license
+
+authors:
+- Harald Kirschner
+- Aaron Newton
+
+requires:
+- /Keyboard
+- /HtmlTable
+- /Class.refactor
+- /Element.Delegation
+
+provides: [HtmlTable.Select]
+
+...
+*/
+
+HtmlTable = Class.refactor(HtmlTable, {
+
+ options: {
+ /*onRowSelect: $empty,
+ onRowUnselect: $empty,*/
+ useKeyboard: true,
+ classRowSelected: 'table-tr-selected',
+ classRowHovered: 'table-tr-hovered',
+ classSelectable: 'table-selectable',
+ allowMultiSelect: true,
+ selectable: false
+ },
+
+ initialize: function(){
+ this.previous.apply(this, arguments);
+ if (this.occluded) return this.occluded;
+ this.selectedRows = new Elements();
+ this.bound = {
+ mouseleave: this.mouseleave.bind(this),
+ focusRow: this.focusRow.bind(this)
+ };
+ if (this.options.selectable) this.enableSelect();
+ },
+
+ enableSelect: function(){
+ this.selectEnabled = true;
+ this.attachSelects();
+ this.element.addClass(this.options.classSelectable);
+ },
+
+ disableSelect: function(){
+ this.selectEnabled = false;
+ this.attach(false);
+ this.element.removeClass(this.options.classSelectable);
+ },
+
+ attachSelects: function(attach){
+ attach = $pick(attach, true);
+ var method = attach ? 'addEvents' : 'removeEvents';
+ this.element[method]({
+ mouseleave: this.bound.mouseleave
+ });
+ this.body[method]({
+ 'click:relay(tr)': this.bound.focusRow
+ });
+ if (this.options.useKeyboard || this.keyboard){
+ if (!this.keyboard) this.keyboard = new Keyboard({
+ events: {
+ down: function(e) {
+ e.preventDefault();
+ this.shiftFocus(1);
+ }.bind(this),
+ up: function(e) {
+ e.preventDefault();
+ this.shiftFocus(-1);
+ }.bind(this),
+ enter: function(e) {
+ e.preventDefault();
+ if (this.hover) this.focusRow(this.hover);
+ }.bind(this)
+ },
+ active: true
+ });
+ this.keyboard[attach ? 'activate' : 'deactivate']();
+ }
+ this.updateSelects();
+ },
+
+ mouseleave: function(){
+ if (this.hover) this.leaveRow(this.hover);
+ },
+
+ focus: function(){
+ if (this.keyboard) this.keyboard.activate();
+ },
+
+ blur: function(){
+ if (this.keyboard) this.keyboard.deactivate();
+ },
+
+ push: function(){
+ var ret = this.previous.apply(this, arguments);
+ this.updateSelects();
+ return ret;
+ },
+
+ updateSelects: function(){
+ Array.each(this.body.rows, function(row){
+ var binders = row.retrieve('binders');
+ if ((binders && this.selectEnabled) || (!binders && !this.selectEnabled)) return;
+ if (!binders){
+ binders = {
+ mouseenter: this.enterRow.bind(this, [row]),
+ mouseleave: this.leaveRow.bind(this, [row])
+ };
+ row.store('binders', binders).addEvents(binders);
+ } else {
+ row.removeEvents(binders);
+ }
+ }, this);
+ },
+
+ enterRow: function(row){
+ if (this.hover) this.hover = this.leaveRow(this.hover);
+ this.hover = row.addClass(this.options.classRowHovered);
+ },
+
+ shiftFocus: function(offset){
+ if (!this.hover) return this.enterRow(this.body.rows[0]);
+ var to = Array.indexOf(this.body.rows, this.hover) + offset;
+ if (to < 0) to = 0;
+ if (to >= this.body.rows.length) to = this.body.rows.length - 1;
+ if (this.hover == this.body.rows[to]) return this;
+ this.enterRow(this.body.rows[to]);
+ },
+
+ leaveRow: function(row){
+ row.removeClass(this.options.classRowHovered);
+ },
+
+ focusRow: function(){
+ var row = arguments[1] || arguments[0]; //delegation passes the event first
+ if (!this.body.getChildren().contains(row)) return;
+ var unfocus = function(row){
+ this.selectedRows.erase(row);
+ row.removeClass(this.options.classRowSelected);
+ this.fireEvent('rowUnfocus', [row, this.selectedRows]);
+ }.bind(this);
+ if (!this.options.allowMultiSelect) this.selectedRows.each(unfocus);
+ if (!this.selectedRows.contains(row)) {
+ this.selectedRows.push(row);
+ row.addClass(this.options.classRowSelected);
+ this.fireEvent('rowFocus', [row, this.selectedRows]);
+ } else {
+ unfocus(row);
+ }
+ return false;
+ },
+
+ selectAll: function(status){
+ status = $pick(status, true);
+ if (!this.options.allowMultiSelect && status) return;
+ if (!status) this.selectedRows.removeClass(this.options.classRowSelected).empty();
+ else this.selectedRows.combine(this.body.rows).addClass(this.options.classRowSelected);
+ return this;
+ },
+
+ selectNone: function(){
+ return this.selectAll(false);
+ }
+
+});
+
+/*
+---
+
+script: Keyboard.js
+
+description: KeyboardEvents used to intercept events on a class for keyboard and format modifiers in a specific order so as to make alt+shift+c the same as shift+alt+c.
+
+license: MIT-style license
+
+authors:
+- Perrin Westrich
+- Aaron Newton
+- Scott Kyle
+
+requires:
+- core:1.2.4/Events
+- core:1.2.4/Options
+- core:1.2.4/Element.Event
+- /Log
+
+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({
+
+ Extends: Events,
+
+ Implements: [Options, Log],
+
+ options: {
+ /*
+ onActivate: $empty,
+ onDeactivate: $empty,
+ */
+ defaultEventType: 'keydown',
+ active: false,
+ events: {}
+ },
+
+ 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.options.active) this.activate();
+ },
+
+ handle: function(event, type){
+ //Keyboard.stop(event) prevents key propagation
+ if (!this.active || event.preventKeyboardPropagation) return;
+
+ var bubbles = !!this.manager;
+ if (bubbles && this.activeKB){
+ this.activeKB.handle(event, type);
+ if (event.preventKeyboardPropagation) return;
+ }
+ this.fireEvent(type, event);
+
+ if (!bubbles && this.activeKB) this.activeKB.handle(event, type);
+ },
+
+ addEvent: function(type, fn, internal) {
+ return this.parent(parse(type, this.options.defaultEventType), 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');
+ },
+
+ toggleActive: function(){
+ return this[this.active ? 'deactivate' : 'activate']();
+ },
+
+ enable: 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');
+ } else if (this.manager) {
+ //else we're enabling ourselves, we must ask our parent to do it for us
+ this.manager.enable(this);
+ }
+ return this;
+ },
+
+ relenquish: function(){
+ if (this.previous) this.enable(this.previous);
+ },
+
+ //management logic
+ manage: function(instance) {
+ if (instance.manager) instance.manager.drop(instance);
+ this.instances.push(instance);
+ instance.manager = this;
+ if (!this.activeKB) this.enable(instance);
+ else this._disable(instance);
+ },
+
+ _disable: function(instance) {
+ if (this.activeKB == instance) this.activeKB = null;
+ },
+
+ drop: function(instance) {
+ this._disable(instance);
+ this.instances.erase(instance);
+ },
+
+ 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.stop = function(event) {
+ event.preventKeyboardPropagation = true;
+ };
+
+ Keyboard.manager = new this.Keyboard({
+ active: true
+ });
+
+ Keyboard.trace = function(){
+ Keyboard.manager.trace();
+ };
+
+ var handler = function(event){
+ var mods = '';
+ modifiers.each(function(mod){
+ if (event[mod]) mods += mod + '+';
+ });
+ Keyboard.manager.handle(event, event.type + ':' + mods + event.key);
+ };
+
+ document.addEvents({
+ 'keyup': handler,
+ 'keydown': handler
+ });
+
+ Event.Keys.extend({
+ 'pageup': 33,
+ 'pagedown': 34,
+ 'end': 35,
+ 'home': 36,
+ 'capslock': 20,
+ 'numlock': 144,
+ 'scrolllock': 145
+ });
+
+})();
+
+
/*
---
diff --git a/templates/common/tables.html b/templates/common/tables.html
index 1225475e3..efe7808c7 100644
--- a/templates/common/tables.html
+++ b/templates/common/tables.html
@@ -1,18 +1,22 @@
-
-
- {% for c in table.columns %}
- | {{ c|safe }} |
- {% endfor %}
-
- {% for row in table.rows %}
-
- {% for cell in row.cols %}
+
+
+
+ {% for c in table.columns %}
+ | {{ c|safe }} |
+ {% endfor %}
+
+
+
+ {% for row in table.rows %}
+
+ {% for cell in row.cols %}
{% if cell.safe %}
- | {{ cell.value|safe }} |
+ {{ cell.value|safe }} |
{% else %}
- {{ cell.value }} |
+ {{ cell.value }} |
{% endif %}
+ {% endfor %}
+
{% endfor %}
-
- {% endfor %}
+
diff --git a/templates/nlayout.html b/templates/nlayout.html
index f86d4ab33..77448071b 100644
--- a/templates/nlayout.html
+++ b/templates/nlayout.html
@@ -11,6 +11,7 @@
+
{% url django.views.i18n.javascript_catalog as catalog_url %}