Consistently use lowercase file names.
authorJan Schneider <jan@horde.org>
Thu, 20 May 2010 22:19:03 +0000 (00:19 +0200)
committerJan Schneider <jan@horde.org>
Thu, 20 May 2010 22:19:21 +0000 (00:19 +0200)
50 files changed:
ansel/js/embed.js [changed mode: 0755->0644]
ansel/js/lightbox.js [changed mode: 0755->0644]
ansel/js/slideshow.js [changed mode: 0755->0644]
ansel/js/slugcheck.js [changed mode: 0755->0644]
ansel/js/tagactions.js [changed mode: 0755->0644]
ansel/js/togglewidget.js [changed mode: 0755->0644]
chora/browsefile.php
chora/patchsets.php
framework/Ajax/lib/Horde/Ajax/Imple/AutoCompleter.php
framework/Ajax/lib/Horde/Ajax/Imple/SpellChecker.php
horde/js/ContextSensitive.js [deleted file]
horde/js/Growler.js [deleted file]
horde/js/KeyNavList.js [deleted file]
horde/js/QuickFinder.js [deleted file]
horde/js/SpellChecker.js [deleted file]
horde/js/TextareaResize.js [deleted file]
horde/js/autocomplete.js
horde/js/contextsensitive.js [new file with mode: 0644]
horde/js/dhtmlHistory.js [deleted file]
horde/js/dhtmlhistory.js [new file with mode: 0644]
horde/js/growler.js [new file with mode: 0644]
horde/js/ieEscGuard.js [deleted file]
horde/js/ieescguard.js [new file with mode: 0644]
horde/js/keynavlist.js [new file with mode: 0644]
horde/js/quickfinder.js [new file with mode: 0644]
horde/js/textarearesize.js [new file with mode: 0644]
imp/compose-dimp.php
imp/compose.php
imp/index-dimp.php
imp/js/DimpBase.js [deleted file]
imp/js/DimpCore.js [deleted file]
imp/js/ViewPort.js [deleted file]
imp/js/dimpbase.js [new file with mode: 0644]
imp/js/dimpcore.js [new file with mode: 0644]
imp/js/viewport.js [new file with mode: 0644]
imp/lib/Ajax/Application.php
imp/lib/Dimp.php
imp/message-dimp.php
jonah/channels/index.php
kronolith/lib/Kronolith.php
kronolith/templates/panel.inc
mnemo/list.php
mnemo/notes/index.php
nag/list.php
nag/tasks/index.php
skoli/list.php
skoli/search.php
skoli/templates/panel.inc
turba/lib/View/Browse.php
turba/search.php

old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 74f324b..0748e58 100644 (file)
@@ -48,7 +48,7 @@ if ($VC->hasFeature('branches')) {
 }
 
 Horde::addScriptFile('tables.js', 'horde');
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 Horde::addScriptFile('revlog.js', 'chora');
 require CHORA_TEMPLATES . '/common-header.inc';
 require CHORA_TEMPLATES . '/menu.inc';
index 91b4863..7bc7196 100644 (file)
@@ -52,7 +52,7 @@ Horde::addScriptFile('tables.js', 'horde');
 
 // JS search not needed if showing a single patchset
 if ($ps_id) {
-    Horde::addScriptFile('QuickFinder.js', 'horde');
+    Horde::addScriptFile('quickfinder.js', 'horde');
 }
 
 require CHORA_TEMPLATES . '/common-header.inc';
index 9513338..8d61c11 100644 (file)
@@ -46,7 +46,7 @@ abstract class Horde_Ajax_Imple_AutoCompleter extends Horde_Ajax_Imple_Base
         $config = $this->_attach(array('tokens' => array(',', ';')));
 
         Horde::addScriptFile('autocomplete.js', 'horde');
-        Horde::addScriptFile('KeyNavList.js', 'horde');
+        Horde::addScriptFile('keynavlist.js', 'horde');
         Horde::addScriptFile('liquidmetal.js', 'horde');
         if (isset($config['ajax'])) {
             $func = 'Ajax.Autocompleter';
index be51f6d..81e7962 100644 (file)
@@ -57,8 +57,8 @@ class Horde_Ajax_Imple_SpellChecker extends Horde_Ajax_Imple_Base
     {
         Horde::addScriptFile('prototype.js', 'horde');
         Horde::addScriptFile('effects.js', 'horde');
-        Horde::addScriptFile('KeyNavList.js', 'horde');
-        Horde::addScriptFile('SpellChecker.js', 'horde');
+        Horde::addScriptFile('keynavlist.js', 'horde');
+        Horde::addScriptFile('spellchecker.js', 'horde');
 
         $opts = array(
             'locales' => $this->_params['locales'],
diff --git a/horde/js/ContextSensitive.js b/horde/js/ContextSensitive.js
deleted file mode 100644 (file)
index 8c76ae9..0000000
+++ /dev/null
@@ -1,388 +0,0 @@
-/**
- * ContextSensitive: a library for generating context-sensitive content on
- * HTML elements. It will take over the click/oncontextmenu functions for the
- * document, and works only where these are possible to override.  It allows
- * contextmenus to be created via both a left and right mouse click.
- *
- * On Opera, the context menu is triggered by a left click + SHIFT + CTRL
- * combination.
- *
- * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only).
- *
- *
- * Usage:
- * ------
- * cs = new ContextSensitive();
- *
- * Custom Events:
- * --------------
- * Custom events are triggered on the base element. The parameters given
- * below are available through the 'memo' property of the Event object.
- *
- * ContextSensitive:click
- *   Fired when a contextmenu element is clicked on.
- *   params: (object) elt - (Element) The menu element clicked on.
- *                    trigger - (string) The parent menu.
- *
- * ContextSensitive:show
- *   Fired before a contextmenu is displayed.
- *   params: (string) The DOM ID of the context menu.
- *
- *
- * Original code by Havard Eide (http://eide.org/) released under the MIT
- * license.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- *
- * @author Chuck Hagenbuch <chuck@horde.org>
- * @author Michael Slusarz <slusarz@horde.org>
- */
-
-var ContextSensitive = Class.create({
-
-    initialize: function()
-    {
-        this.baseelt = null;
-        this.current = [];
-        this.elements = $H();
-        this.submenus = $H();
-        this.triggers = [];
-
-        if (!Prototype.Browser.Opera) {
-            document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this));
-        }
-        document.observe('click', this._leftClickHandler.bindAsEventListener(this));
-        document.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousescroll', this.close.bind(this));
-    },
-
-    /**
-     * Elements are of type ContextSensitive.Element.
-     */
-    addElement: function(id, target, opts)
-    {
-        var left = Boolean(opts.left);
-        if (id && !this.validElement(id, left)) {
-            this.elements.set(id + Number(left), new ContextSensitive.Element(id, target, opts));
-        }
-    },
-
-    /**
-     * Remove a registered element.
-     */
-    removeElement: function(id)
-    {
-        this.elements.unset(id + '0');
-        this.elements.unset(id + '1');
-    },
-
-    /**
-     * Hide the currently displayed element(s).
-     */
-    close: function()
-    {
-        this._closeMenu(0, true);
-    },
-
-    /**
-     * Close all menus below a specified level.
-     */
-    _closeMenu: function(idx, immediate)
-    {
-        if (this.current.size()) {
-            this.current.splice(idx, this.current.size() - idx).each(function(s) {
-                // Fade-out on final display.
-                if (!immediate && idx == 0) {
-                    s.fade({ duration: 0.15 });
-                } else {
-                    $(s).hide();
-                }
-            });
-
-            this.triggers.splice(idx, this.triggers.size() - idx).each(function(s) {
-                $(s).removeClassName('contextHover');
-            });
-
-            if (idx == 0) {
-                this.baseelt = null;
-            }
-        }
-    },
-
-    /**
-     * Returns the current displayed menu element ID, if any. If more than one
-     * submenu is open, returns the last ID opened.
-     */
-    currentmenu: function()
-    {
-        return this.current.last();
-    },
-
-    /**
-     * Get a valid element (the ones that can be right-clicked) based
-     * on a element ID.
-     */
-    validElement: function(id, left)
-    {
-        return this.elements.get(id + Number(Boolean(left)));
-    },
-
-    /**
-     * Set the disabled flag of an event.
-     */
-    disable: function(id, left, disable)
-    {
-        var e = this.validElement(id, left);
-        if (e) {
-            e.disable = disable;
-        }
-    },
-
-    /**
-     * Called when a left click event occurs. Will return before the
-     * element is closed if we click on an element inside of it.
-     */
-    _leftClickHandler: function(e)
-    {
-        var base, elt, elt_up, trigger;
-
-        if (this.operaCheck(e)) {
-            this._rightClickHandler(e, false);
-            e.stop();
-            return;
-        }
-
-        // Check for a right click. FF on Linux triggers an onclick event even
-        // w/a right click, so disregard.
-        if (e.isRightClick()) {
-            return;
-        }
-
-        // Check for click in open contextmenu.
-        if (this.current.size()) {
-            elt = e.element();
-            if (!elt.match('A')) {
-                elt = elt.up('A');
-                if (!elt) {
-                    this._rightClickHandler(e, true);
-                    return;
-                }
-            }
-            elt_up = elt.up('.contextMenu');
-
-            if (elt_up) {
-                e.stop();
-
-                if (elt.hasClassName('contextSubmenu') &&
-                    elt_up.identify() != this.currentmenu()) {
-                    this._closeMenu(this.current.indexOf(elt.identify()));
-                }
-
-                base = this.baseelt;
-                trigger = this.triggers.last();
-                this.close();
-                base.fire('ContextSensitive:click', { elt: elt, trigger: trigger });
-                return;
-            }
-        }
-
-        // Check if the mouseclick is registered to an element now.
-        this._rightClickHandler(e, true);
-    },
-
-    /**
-     * Checks if the Opera right-click emulation is present.
-     */
-    operaCheck: function(e)
-    {
-        return Prototype.Browser.Opera && e.shiftKey && e.ctrlKey;
-    },
-
-    /**
-     * Called when a right click event occurs.
-     */
-    _rightClickHandler: function(e, left)
-    {
-        if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) {
-            e.stop();
-        }
-    },
-
-    /**
-     * Display context menu if valid element has been activated.
-     */
-    trigger: function(target, leftclick, x, y)
-    {
-        var ctx, el, offset, offsets, voffsets;
-
-        [ target ].concat(target.ancestors()).find(function(n) {
-            ctx = this.validElement(n.id, leftclick);
-            return ctx;
-        }, this);
-
-        // Try to retrieve the context-sensitive element we want to
-        // display. If we can't find it we just return.
-        if (!ctx ||
-            ctx.disable ||
-            !(el = $(ctx.ctx)) ||
-            (leftclick && target == this.baseelt) ||
-            this.currentmenu() == ctx.ctx) {
-            this.close();
-            return false;
-        }
-
-        this.close();
-
-        // Register the element that was clicked on.
-        this.baseelt = target;
-
-        offset = ctx.opts.offset;
-        if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) {
-            offset = target.identify();
-        }
-        offset = $(offset);
-
-        if (offset) {
-            offsets = offset.viewportOffset();
-            voffsets = document.viewport.getScrollOffsets();
-            x = offsets[0] + voffsets.left;
-            y = offsets[1] + offset.getHeight() + voffsets.top;
-        }
-
-        this._displayMenu(el, x, y);
-        this.triggers.push(el.identify());
-
-        return true;
-    },
-
-    /**
-     * Display the [sub]menu on the screen.
-     */
-    _displayMenu: function(elt, x, y)
-    {
-        // Get window/element dimensions
-        var eltL, h, w,
-            id = elt.identify(),
-            v = document.viewport.getDimensions();
-
-        elt.setStyle({ visibility: 'hidden' }).show();
-        eltL = elt.getLayout(),
-        h = eltL.get('border-box-height');
-        w = eltL.get('border-box-width');
-        elt.hide().setStyle({ visibility: 'visible' });
-
-        // Make sure context window is entirely on screen
-        if ((y + h) > v.height) {
-            y = v.height - h - 2;
-        }
-
-        if ((x + w) > v.width) {
-            x = this.current.size()
-                ? ($(this.current.last()).viewportOffset()[0] - w)
-                : (v.width - w - 2);
-        }
-
-        this.baseelt.fire('ContextSensitive:show', id);
-
-        elt.setStyle({ left: x + 'px', top: y + 'px' })
-
-        if (this.current.size()) {
-            elt.show();
-        } else {
-            // Fade-in on initial display.
-            elt.appear({ duration: 0.15 });
-        }
-
-        this.current.push(id);
-    },
-
-    /**
-     * Add a submenu to an existing menu.
-     */
-    addSubMenu: function(id, submenu)
-    {
-        if (!this.submenus.get(id)) {
-            if (!this.submenus.size()) {
-                document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this));
-            }
-            this.submenus.set(id, submenu);
-            $(submenu).addClassName('contextMenu');
-            $(id).addClassName('contextSubmenu');
-        }
-    },
-
-    /**
-     * Mouseover DOM Event handler.
-     */
-    _mouseoverHandler: function(e)
-    {
-        if (!this.current.size()) {
-            return;
-        }
-
-        var cm = this.currentmenu(),
-            elt = e.element(),
-            elt_up = elt.up('.contextMenu'),
-            id = elt.identify(),
-            id_div, offsets, sub, voffsets, x, y;
-
-        if (!elt_up) {
-            return;
-        }
-
-        id_div = elt_up.identify();
-
-        if (elt.hasClassName('contextSubmenu')) {
-            sub = this.submenus.get(id);
-            if (sub != cm || this.currentmenu() != id) {
-                if (id_div != cm) {
-                    this._closeMenu(this.current.indexOf(id_div) + 1);
-                }
-
-                offsets = elt.viewportOffset();
-                voffsets = document.viewport.getScrollOffsets();
-                x = offsets[0] + voffsets.left + elt.getWidth();
-                y = offsets[1] + voffsets.top;
-                this._displayMenu($(sub), x, y, id);
-                this.triggers.push(id);
-                elt.addClassName('contextHover');
-            }
-        } else if ((this.current.size() > 1) &&
-                   id_div != cm) {
-            this._closeMenu(this.current.indexOf(id));
-        }
-    }
-
-});
-
-ContextSensitive.Element = Class.create({
-
-    // opts: 'left' -> monitor left click; 'offset' -> id of element used to
-    //       determine offset placement
-    initialize: function(id, target, opts)
-    {
-        this.id = id;
-        this.ctx = target;
-        this.opts = opts;
-        this.opts.left = Boolean(opts.left);
-        this.disable = false;
-
-        target = $(target);
-        if (target) {
-            target.addClassName('contextMenu');
-        }
-    }
-
-});
diff --git a/horde/js/Growler.js b/horde/js/Growler.js
deleted file mode 100644 (file)
index 02ace41..0000000
+++ /dev/null
@@ -1,283 +0,0 @@
-/**
- * Growler.js - Display 'Growl'-like notifications.
- *
- * Notice Options (passed to 'Growler.growl()'):
- * 'className' - (string) An optional additional CSS class to apply to notice.
- * 'header' - (string) The title that is displayed for the notice.
- * 'life' - (float) The number of seconds in which the notice remains visible.
- * 'log' - (boolean) If true, will log the entry.
- * 'speedin' - (float) The speed in seconds in which the notice is shown.
- * 'speedout' - (float) The speed in seconds in which the notice is hidden.
- * 'sticky' - (boolean) Determines if the notice should always remain visible
- *            until closed by the user.
- *
- * Growler Options (passed to 'new Growler()'):
- * 'location' - (string) The location of the growler notices. This can be:
- *   tr (top-right)
- *   br (bottom-right)
- *   tl (top-left)
- *   bl (bottom-left)
- *   tc (top-center)
- *   bc (bottom-center)
- * 'log' - (boolean) Enable logging.
- * 'noalerts' - (string) The localized string to display when no log entries
- *              are present.
- *
- * Custom Events:
- * --------------
- * Custom events are triggered on the notice element. The parameters given
- * below are available through the 'memo' property of the Event object.
- *
- * Growler:created
- *   Fired on TODO
- *   params: NONE
- *
- * Growler:destroyed
- *   Fired on TODO
- *   params: NONE
- *
- *
- * Growler has been tested with Safari 3(Mac|Win), Firefox 3(Mac|Win), IE6,
- * IE7, and Opera.
- *
- * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only).
- *
- * Code adapted from k.Growler v1.0.0
- *   http://code.google.com/p/kproto/
- *   Written by Kevin Armstrong <kevin@kevinandre.com>
- *   Released under the MIT license
- *
- * @author Michael Slusarz <slusarz@horde.org>
- */
-
-(function() {
-
-    var noticeOptions = {
-        header: '',
-        speedin: 0.3,
-        speedout: 0.5,
-        life: 5,
-        sticky: false,
-        className: ''
-    },
-
-    growlerOptions = {
-        location: 'tr',
-        log: false,
-        noalerts: 'No Alerts'
-    },
-
-    IE6 = Prototype.Browser.IE
-        ? (parseFloat(navigator.appVersion.split("MSIE ")[1]) || 0) == 6
-        : 0;
-
-    function removeNotice(n, o)
-    {
-        o = o || noticeOptions;
-
-        $(n).fade({
-            duration: o.speedout,
-            afterFinish: function() {
-                try {
-                    var ne = n.down('DIV.GrowlerNoticeExit');
-                    if (!Object.isUndefined(ne)) {
-                        ne.stopObserving('click', removeNotice);
-                    }
-                    n.fire('Growler:destroyed');
-                } catch (e) {}
-
-                try {
-                    n.remove();
-                    if (!$('Growler').childElements().size()) {
-                        $('Growler').hide().setOpacity(1);
-                    }
-                } catch (e) {}
-            }
-        });
-    }
-
-    function removeLog(l)
-    {
-        try {
-            var le = l.down('DIV.GrowlerNoticeExit');
-            if (!Object.isUndefined(le)) {
-                le.stopObserving('click', removeLog);
-            }
-        } catch (e) {}
-        try {
-            l.remove();
-        } catch (e) {}
-    }
-
-    window.Growler = Class.create({
-
-        initialize: function(opts)
-        {
-            var ch, cw, sl, st;
-            opts = Object.extend(Object.clone(growlerOptions), opts || {});
-
-            this.growler = new Element('DIV', { id: 'Growler' }).setStyle({ position: IE6 ? 'absolute' : 'fixed', padding: '10px', zIndex: 50000 }).hide();
-
-            if (IE6) {
-                ch = '0 - this.offsetHeight + ( document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight )';
-                cw = '0 - this.offsetWidth + ( document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth )';
-                sl = '( document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft )';
-                st = '( document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )';
-            } else if (opts.log) {
-                this.growlerlog = new Element('DIV', { id: 'GrowlerLog' }).insert(new Element('DIV').hide().insert(new Element('UL').insert(new Element('LI', { className: 'NoAlerts' }).insert(opts.noalerts))));
-                $(document.body).insert(this.growlerlog);
-            }
-
-            switch (opts.location) {
-            case 'br':
-                if (IE6) {
-                    this.growler.style.setExpression('left', "( " + cw + " + " + sl + " ) + 'px'");
-                    this.growler.style.setExpression('top', "( " + ch + "+ " + st + " ) + 'px'");
-                } else {
-                    this.growler.setStyle({ bottom: 0, right: 0 });
-                }
-                break;
-
-            case 'tl':
-                if (IE6) {
-                    this.growler.style.setExpression('left', sl + " + 'px'");
-                    this.growler.style.setExpression('top', st + " + 'px'");
-                } else {
-                    this.growler.setStyle({ top: 0, left: 0 });
-                }
-                break;
-
-            case 'bl':
-                if (IE6) {
-                    this.growler.style.setExpression('left', sl + " + 'px'");
-                    this.growler.style.setExpression('top', "( " + ch + " + " + st + " ) + 'px'");
-                } else {
-                    this.growler.setStyle({ top: 0, right: 0 });
-                }
-                break;
-
-            case 'tc':
-                if (!IE6) {
-                    this.growler.setStyle({ top: 0, left: '25%', width: '50%' });
-                }
-                break;
-
-            case 'bc':
-                if (!IE6) {
-                    this.growler.setStyle({ bottom: 0, left: '25%', width: '50%' });
-                }
-                break;
-
-            default:
-                if (IE6) {
-                    this.growler.setStyle({ bottom: 'auto', right: 'auto' });
-                    this.growler.style.setExpression('left', "( " + cw + " + " + sl + " ) + 'px'");
-                    this.growler.style.setExpression('top', st + " + 'px'");
-                } else {
-                    this.growler.setStyle({ top: 0, right: 0 });
-                }
-                break;
-            }
-
-            this.growler.wrap(document.body);
-
-            this.growler.observe('mouseenter', function() {
-                this.growler.fade({
-                    duration: 0.25,
-                    queue: { limit: 2, scope: 'growler' },
-                    to: 0.3
-                });
-            }.bind(this));
-            this.growler.observe('mouseleave', function() {
-                this.growler.appear({
-                    duration: 0.25,
-                    queue: { limit: 2, scope: 'growler' },
-                    to: 1
-                });
-            }.bind(this));
-        },
-
-        growl: function(msg, options)
-        {
-            options = options || {};
-            var notice, noticeExit, log, logExit, tmp,
-                opts = Object.clone(noticeOptions);
-            Object.extend(opts, options);
-
-            if (opts.log && this.growlerlog) {
-                tmp = this.growlerlog.down('DIV UL');
-                if (tmp.down().hasClassName('NoAlerts')) {
-                    tmp.down().remove();
-                }
-                log = new Element('LI', { className: opts.className.empty() ? null : opts.className }).insert(msg).insert(new Element('SPAN', { className: 'alertdate'} ).insert('[' + (new Date).toLocaleString() + ']'));
-                logExit = new Element('DIV', { className: 'GrowlerNoticeExit' }).update("&times;");
-                logExit.observe('click', removeLog.curry(log));
-                log.insert(logExit);
-                tmp.insert(log);
-            }
-
-            notice = new Element('DIV', { className: 'GrowlerNotice' }).setStyle({ display: 'block', opacity: 0 });
-            if (!opts.className.empty()) {
-                notice.addClassName(opts.className);
-            }
-
-            noticeExit = new Element('DIV', { className: 'GrowlerNoticeExit' }).update("&times;");
-            noticeExit.observe('click', removeNotice.curry(notice, opts));
-            notice.insert(noticeExit);
-
-            if (!opts.header.empty()) {
-                notice.insert(new Element('DIV', { className: 'GrowlerNoticeHead' }).update(opts.header))
-            }
-
-            notice.insert(new Element('DIV', { className: 'GrowlerNoticeBody' }).update(msg));
-
-            this.growler.show().insert(notice);
-
-            new Effect.Opacity(notice, { to: 0.85, duration: opts.speedin });
-
-            if (!opts.sticky) {
-                removeNotice.delay(opts.life, notice, opts);
-            }
-
-            notice.fire('Growler:created');
-
-            return notice;
-        },
-
-        ungrowl: function(n, o)
-        {
-            removeNotice(n, o);
-        },
-
-        toggleLog: function()
-        {
-            if (!this.growlerlog) {
-                return;
-            }
-            Effect.toggle(this.growlerlog.down('DIV'), 'blind', {
-                duration: 0.5,
-                queue: {
-                    position: 'end',
-                    scope: 'GrowlerLog',
-                    limit: 2
-                }
-            });
-            this.logvisible = !this.logvisible;
-            return this.logvisible;
-        },
-
-        logVisible: function()
-        {
-            return this.growlerlog && this.logvisible;
-        },
-
-        logSize: function()
-        {
-            return (this.growlerlog && this.growlerlog.down('.NoAlerts'))
-                ? 0
-                : this.growlerlog.down('UL').childElements().size();
-        }
-
-    });
-
-})();
diff --git a/horde/js/KeyNavList.js b/horde/js/KeyNavList.js
deleted file mode 100644 (file)
index 58c4c27..0000000
+++ /dev/null
@@ -1,288 +0,0 @@
-/**
- * Reuseable keyboard or mouse driven list component. Based on
- * Scriptaculous' AutoCompleter.
- *
- * Requires: prototype.js (v1.6.1+)
- *
- * Usage:
- * ======
- * var knl = new KeyNavList(base, {
- *     'esc' - (boolean) Escape the displayed output?
- *     'list' - (array) Array of objects with the following keys:
- *                      'l' - (label) Display data
- *                      's' - (selected) True if this entry should be selected
- *                            by default
- *                      'v' - (value) Value of entry
- *     'onChoose' - (function) Called when an entry is selected. Passed the
- *                             entry value.
- *     'onHide' - (function) Called when the list is hidden. Passed the
- *                           list container element.
- *     'onShow' - (function) Called when the list is shown. Passed the
- *                           list container element.
- *     'domParent' - (Element) Specifies the parent element. Defaults to
- *                              document.body
- *     'keydownObserver - (Element) The element to register the keydown handler
- *                                  on. Defaults to document.
- * });
- *
- * [base = (Element) The element to use for display positioning purposes]
- *
- * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-var KeyNavList = Class.create({
-
-    // Vars used: active, div, iefix, ignore, onClickFunc, onKeyDownFunc,
-    //            resizeFunc, selected
-
-    initialize: function(base, opts)
-    {
-        this.active = false;
-        this.base = $(base);
-
-        this.opts = Object.extend({
-            onChoose: Prototype.emptyFunction,
-            onHide: Prototype.emptyFunction,
-            onShow: Prototype.emptyFunction,
-            domParent: null,
-            keydownObserver: document
-        }, opts || {});
-
-        this.div = new Element('DIV', { className: 'KeyNavList' }).hide().insert(new Element('UL'));
-
-        if (!this.opts.domParent) {
-            this.opts.domParent = document.body;
-        }
-        this.opts.domParent = $(this.opts.domParent);
-
-        this.opts.domParent.insert(this.div);
-
-        if (this.opts.list) {
-            this.update(this.opts.list);
-            delete this.opts.list;
-        }
-
-        this.onClickFunc = this.onClick.bindAsEventListener(this);
-        document.observe('click', this.onClickFunc);
-
-        this.onKeyDownFunc = this.onKeyDown.bindAsEventListener(this);
-        $(this.opts.keydownObserver).observe('keydown', this.onKeyDownFunc);
-
-        this.resizeFunc = this.hide.bind(this);
-        Event.observe(window, 'resize', this.resizeFunc);
-
-        if (Prototype.Browser.IE && !window.XMLHttpRequest) {
-            this.iefix = $('knl_iframe_iefix');
-            if (!this.iefix) {
-                this.iefix = new Element('IFRAME', { id: 'knl_iframe_iefix', src: 'javascript:false;', scrolling: 'no', frameborder: 0 }).setStyle({ position: 'absolute', display: 'block', zIndex: 99 }).hide();
-                document.body.appendChild(this.iefix);
-            }
-            this.div.setStyle({ zIndex: 100 });
-        }
-    },
-
-    destroy: function()
-    {
-        document.stopObserving('click', this.onClickFunc);
-        document.stopObserving('keydown', this.onKeyDownFunc);
-        Event.stopObserving(window, 'resize', this.resizeFunc);
-        this.div.remove();
-        if (this.iefix) {
-            this.iefix.remove();
-        }
-    },
-
-    update: function(list)
-    {
-        var df = document.createDocumentFragment();
-
-        list.each(function(v) {
-            var li;
-            if (this.opts.esc) {
-                v.l = v.l.escapeHTML().gsub('  ', ' &nbsp;');
-            }
-            li = new Element('LI').insert(v.l).store('v', v.v);
-            if (v.s) {
-                this.markSelected(li);
-            }
-            df.appendChild(li);
-        }.bind(this));
-
-        this.div.down().childElements().invoke('remove');
-        this.div.down().appendChild(df);
-    },
-
-    updateBase: function(base)
-    {
-        this.base = $(base);
-    },
-
-    show: function(list)
-    {
-        this.active = true;
-
-        if (!Object.isUndefined(list)) {
-            this.update(list);
-        } else if (this.div.visible()) {
-            return;
-        }
-
-        this.opts.onShow(this.div);
-
-        this.div.setStyle({ height: null, width: null, top: null }).clonePosition(this.base, {
-            setHeight: false,
-            setWidth: false,
-            offsetTop: this.base.getHeight()
-        });
-
-        if (this.div.visible()) {
-            this._sizeDiv();
-        } else {
-            this.div.appear({
-                afterFinish: function() {
-                    if (this.selected) {
-                        this.div.scrollTop = this.selected.offsetTop;
-                    }
-                }.bind(this),
-                afterSetup: this._sizeDiv.bind(this),
-                duration: 0.15
-            });
-        }
-    },
-
-    _sizeDiv: function()
-    {
-        var divL = this.div.getLayout(),
-            dl = divL.get('left'),
-            dt = divL.get('top'),
-            off = this.opts.domParent.cumulativeOffset(),
-            v = document.viewport.getDimensions();
-
-        if ((divL.get('border-box-height') + dt + off.top + 10) > v.height) {
-            this.div.setStyle({
-                height: (v.height - dt - off.top - 10) + 'px',
-                width: (this.div.scrollWidth + 5) + 'px'
-            });
-        }
-
-        /* Need to do width second - horizontal scrolling might add scroll
-         * bar. */
-        if ((divL.get('border-box-width') + dl + off.left + 5) > v.width) {
-            dl = (v.width - divL.get('border-box-width') - off.left - 5);
-            this.div.setStyle({ left: dl + 'px' });
-        }
-
-        if (this.iefix) {
-            this.iefix.clonePosition(this.div);
-        }
-    },
-
-    hide: function()
-    {
-        if (this.div.visible()) {
-            this.active = false;
-            this.opts.onHide(this.div);
-            this.div.fade({ duration: 0.15 });
-            if (this.iefix) {
-                this.iefix.hide();
-            }
-        }
-    },
-
-    onKeyDown: function(e)
-    {
-        if (!this.active) {
-            return;
-        }
-
-        switch (e.keyCode) {
-        case Event.KEY_TAB:
-        case Event.KEY_RETURN:
-            this.opts.onChoose(this.getCurrentEntry());
-            this.hide();
-            e.stop();
-            return;
-
-        case Event.KEY_ESC:
-            this.hide();
-            e.stop();
-            return;
-
-        case Event.KEY_UP:
-            this.markPrevious();
-            e.stop();
-            return;
-
-        case Event.KEY_DOWN:
-            this.markNext();
-            e.stop();
-            return;
-        }
-    },
-
-    onClick: function(e)
-    {
-        if (this.active && this.ignore != e) {
-            var elt = e.findElement('LI');
-
-            if (elt &&
-                (elt == this.div || elt.descendantOf(this.div))) {
-                this.markSelected(elt);
-                this.opts.onChoose(this.getCurrentEntry());
-                e.stop();
-            }
-            this.hide();
-        }
-
-        this.ignore = null;
-    },
-
-    ignoreClick: function(e)
-    {
-        this.ignore = e;
-    },
-
-    setSelected: function(value)
-    {
-        this.markSelected(this.div.down().childElements().find(function(e) {
-            return e.retrieve('v') == value;
-        }));
-    },
-
-    markSelected: function(elt)
-    {
-        if (this.selected) {
-            this.selected.removeClassName('selected');
-        }
-        this.selected = elt
-            ? elt.addClassName('selected')
-            : null;
-    },
-
-    markPrevious: function()
-    {
-        this.markSelected(this.selected ? this.selected.previous() : null);
-    },
-
-    markNext: function()
-    {
-        var elt = this.selected
-            ? this.selected.next()
-            : this.div.down().childElements().first();
-
-        if (elt) {
-            this.markSelected(elt);
-        }
-    },
-
-    getCurrentEntry: function()
-    {
-        return this.selected
-            ? this.selected.retrieve('v')
-            : null;
-    }
-
-});
diff --git a/horde/js/QuickFinder.js b/horde/js/QuickFinder.js
deleted file mode 100644 (file)
index 0233252..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Component for filtering a table or any list of children based on
- * the dynamic value of a text input. It requires the prototype.js
- * library.
- *
- * You should define the CSS class .QuickFinderNoMatch to say what
- * happens to items that don't match the criteria. A reasonable
- * default would be display:none.
- *
- * This code is heavily inspired by Filterlicious by Gavin
- * Kistner. The filterlicious JavaScript file did not have a license;
- * however, most of Gavin's code is under the license defined by
- * http://phrogz.net/JS/_ReuseLicense.txt, so I'm including that URL
- * and Gavin's name as acknowledgements.
- *
- * @author Chuck Hagenbuch <chuck@horde.org>
- */
-
-var QuickFinder = {
-
-    attachBehavior: function(input) {
-        var filterTarget = input.readAttribute('for');
-        if (!filterTarget) {
-            return;
-        }
-
-        if (filterTarget.indexOf(',') != -1) {
-            input.filterTargets = [];
-            var targets = filterTarget.split(',');
-            for (var i = 0; i < targets.length; ++i) {
-                var t = $(targets[i]);
-                if (t) {
-                    input.filterTargets.push(t);
-                }
-            }
-            if (!input.filterTargets.size()) {
-                return;
-            }
-        } else {
-            input.filterTargets = [ $(filterTarget) ];
-            if (!input.filterTargets[0]) {
-                return;
-            }
-        }
-
-        var filterEmpty = input.readAttribute('empty');
-        if (filterEmpty) {
-            input.filterEmpty = $(filterEmpty);
-        }
-
-        input.observe('keyup', this.onKeyUp.bindAsEventListener(this));
-
-        for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) {
-            input.filterTargets[i].childElements().each(function(line) {
-                var filterText = line.filterText || line.readAttribute('filterText');
-                if (!filterText) {
-                    line.filterText = line.innerHTML.stripTags();
-                }
-                line.filterText = line.filterText.toLowerCase();
-            });
-        }
-
-        this.filter(input);
-    },
-
-    onKeyUp: function(e) {
-        var input = e.element();
-        if (input.filterTargets) {
-            this.filter(input);
-        }
-    },
-
-    filter: function(input) {
-        var matched = 0,
-            val = input.value.toLowerCase();
-        for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) {
-            input.filterTargets[i].childElements().each(function(line) {
-                var filterText = line.filterText;
-                if (filterText.indexOf(val) == -1) {
-                    line.addClassName('QuickFinderNoMatch');
-                } else {
-                    ++matched;
-                    line.removeClassName('QuickFinderNoMatch');
-                }
-            });
-        }
-
-        try {
-            if (input.filterEmpty) {
-                (matched == 0) ? input.filterEmpty.show() : input.filterEmpty.hide();
-            }
-        } catch (e) {}
-    }
-
-}
-
-document.observe('dom:loaded', function() {
-    $$('input').each(QuickFinder.attachBehavior.bind(QuickFinder));
-});
diff --git a/horde/js/SpellChecker.js b/horde/js/SpellChecker.js
deleted file mode 100644 (file)
index 2b8a8c6..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- * This spell checker was inspired by work done by Garrison Locke, but
- * was rewritten almost completely by Chuck Hagenbuch to use
- * Prototype/Scriptaculous.
- *
- * Requires: prototype.js (v1.6.1+), KeyNavList.js
- *
- * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- *
- * Custom Events:
- * --------------
- * Custom events are triggered on the target element.
- *
- * 'SpellChecker:after'
- *    Fired when the spellcheck processing ends.
- *
- * 'SpellChecker:before'
- *    Fired before the spellcheck is performed.
- *
- * 'SpellChecker:noerror'
- *    Fired when no spellcheck errors are found.
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-var SpellChecker = Class.create({
-
-    // Vars used and defaulting to null:
-    //   bad, choices, disabled, htmlAreaParent, lc, locale, reviewDiv,
-    //   statusButton, statusClass, suggestions, target, url
-    options: {},
-    resumeOnDblClick: true,
-    state: 'CheckSpelling',
-
-    // Options:
-    //   bs = (array) Button states
-    //   locales = (array) List of locales. See KeyNavList for structure.
-    //   sc = (string) Status class
-    //   statusButton = (string/element) DOM ID or element of the status
-    //                  button
-    //   target = (string|Element) DOM element containing data
-    //   url = (string) URL of specllchecker handler
-    initialize: function(opts)
-    {
-        var d, lc, tmp, ul;
-
-        this.url = opts.url;
-        this.target = $(opts.target);
-        this.statusButton = $(opts.statusButton);
-        this.buttonStates = opts.bs;
-        this.statusClass = opts.sc || '';
-        this.disabled = false;
-
-        this.options.onComplete = this.onComplete.bind(this);
-
-        document.observe('click', this.onClick.bindAsEventListener(this));
-
-        if (opts.locales) {
-            this.lc = new KeyNavList(this.statusButton, {
-                list: opts.locales,
-                onChoose: this.setLocale.bindAsEventListener(this)
-            });
-
-            this.statusButton.insert({ after: new Element('SPAN', { className: 'spellcheckPopdownImg' }) });
-        }
-
-        this.setStatus('CheckSpelling');
-    },
-
-    setLocale: function(locale)
-    {
-        this.locale = locale;
-    },
-
-    targetValue: function()
-    {
-        return Object.isUndefined(this.target.value)
-            ? this.target.innerHTML
-            : this.target.value;
-    },
-
-    spellCheck: function()
-    {
-        this.target.fire('SpellChecker:before');
-
-        var opts = Object.clone(this.options),
-            p = $H(),
-            url = this.url;
-
-        this.setStatus('Checking');
-
-        p.set(this.target.identify(), this.targetValue());
-        opts.parameters = p.toQueryString();
-
-        if (this.locale) {
-            url += '/locale=' + this.locale;
-        }
-        if (this.htmlAreaParent) {
-            url += '/html=1';
-        }
-
-        new Ajax.Request(url, opts);
-    },
-
-    onComplete: function(request)
-    {
-        var bad, content, washidden,
-            i = 0,
-            result = request.responseJSON;
-
-        if (Object.isUndefined(result)) {
-            this.setStatus('Error');
-            return;
-        }
-
-        this.suggestions = result.suggestions || [];
-
-        if (!this.suggestions.size()) {
-            this.setStatus('CheckSpelling');
-            this.target.fire('SpellChecker:noerror');
-            return;
-        }
-
-        bad = result.bad || [];
-
-        content = this.targetValue();
-        content = this.htmlAreaParent
-            ? content.replace(/\r?\n/g, '')
-            : content.replace(/\r?\n/g, '~~~').escapeHTML();
-
-        $A(bad).each(function(node) {
-            var re_text = '<span index="' + (i++) + '" class="spellcheckIncorrect">' + node + '</span>';
-            content = content.replace(new RegExp("(?:^|\\b)" + RegExp.escape(node) + "(?:\\b|$)", 'g'), re_text);
-
-            // Go through and see if we matched anything inside a tag (i.e.
-            // class/spellcheckIncorrect is often matched if using a
-            // non-English lang).
-            content = content.replace(new RegExp("(<[^>]*)" + RegExp.escape(re_text) + "([^>]*>)", 'g'), '\$1' + node + '\$2');
-        }, this);
-
-        if (!this.reviewDiv) {
-            this.reviewDiv = new Element('DIV', { className: this.target.readAttribute('class') }).addClassName('spellcheck').setStyle({ overflow: 'auto' });
-            if (this.resumeOnDblClick) {
-                this.reviewDiv.observe('dblclick', this.resume.bind(this));
-            }
-        }
-
-        if (!this.target.visible()) {
-            this.target.show();
-            washidden = true;
-        }
-        this.reviewDiv.setStyle({ width: this.target.clientWidth + 'px', height: this.target.clientHeight + 'px'});
-        if (washidden) {
-            this.target.hide();
-        }
-
-        if (!this.htmlAreaParent) {
-            content = content.replace(/~~~/g, '<br />');
-        }
-        this.reviewDiv.update(content);
-
-        if (this.htmlAreaParent) {
-            $(this.htmlAreaParent).insert({ bottom: this.reviewDiv });
-        } else {
-            this.target.hide().insert({ before: this.reviewDiv });
-        }
-
-        this.setStatus('ResumeEdit');
-    },
-
-    onClick: function(e)
-    {
-        var data = [], index, elt = e.element();
-
-        if (this.disabled) {
-            return;
-        }
-
-        if (elt == this.statusButton) {
-            switch (this.state) {
-            case 'CheckSpelling':
-                this.spellCheck();
-                break;
-
-            case 'ResumeEdit':
-                this.resume();
-                break;
-            }
-
-            e.stop();
-        } else if (elt.hasClassName('spellcheckPopdownImg')) {
-            this.lc.show();
-            this.lc.ignoreClick(e);
-            e.stop();
-        } else if (elt.hasClassName('spellcheckIncorrect')) {
-            index = e.element().readAttribute('index');
-
-            $A(this.suggestions[index]).each(function(node) {
-                data.push({ l: node, v: node });
-            });
-
-            if (this.choices) {
-                this.choices.updateBase(elt);
-                this.choices.opts.onChoose = function(val) {elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });};
-            } else {
-                this.choices = new KeyNavList(elt, {
-                    esc: true,
-                    onChoose: function(val) {
-                        elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });
-                    }
-                });
-            }
-
-            this.choices.show(data);
-            this.choices.ignoreClick(e);
-            e.stop();
-        }
-    },
-
-    resume: function()
-    {
-        if (!this.reviewDiv) {
-            return;
-        }
-
-        var t;
-
-        this.reviewDiv.select('span.spellcheckIncorrect').each(function(n) {
-            n.replace(n.innerHTML);
-        });
-
-        t = this.reviewDiv.innerHTML;
-        if (!this.htmlAreaParent) {
-            t = t.replace(/<br *\/?>/gi, '~~~').unescapeHTML().replace(/~~~/g, "\n");
-        }
-        this.target.setValue(t);
-        this.target.enable();
-
-        if (this.resumeOnDblClick) {
-            this.reviewDiv.stopObserving('dblclick');
-        }
-        this.reviewDiv.remove();
-        this.reviewDiv = null;
-
-        this.setStatus('CheckSpelling');
-
-        if (!this.htmlAreaParent) {
-            this.target.show();
-        }
-
-        this.target.fire('SpellChecker:after');
-    },
-
-    setStatus: function(state)
-    {
-        if (!this.statusButton) {
-            return;
-        }
-
-        this.state = state;
-        switch (this.statusButton.tagName) {
-        case 'INPUT':
-            this.statusButton.setValue(this.buttonStates[state]);
-            break;
-
-        case 'A':
-            this.statusButton.update(this.buttonStates[state]);
-            break;
-        }
-        this.statusButton.className = this.statusClass + ' spellcheck' + state;
-    },
-
-    isActive: function()
-    {
-        return this.reviewDiv;
-    },
-
-    disable: function(disable)
-    {
-        this.disabled = disable;
-    }
-
-});
diff --git a/horde/js/TextareaResize.js b/horde/js/TextareaResize.js
deleted file mode 100644 (file)
index 0946f7e..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * TextareaResize: a library that automatically resizes a text area based on
- * its contents.
- *
- * Requires prototypejs 1.6+.
- *
- * Usage:
- * ------
- * cs = new TextareaResize(id[, options]);
- *
- *   id = (string|Element) DOM ID/Element object of textarea.
- *   options = (object) Additional options:
- *      'max_rows' - (Number) The maximum number of rows to display.
- *      'observe_time' - (Number) The interval between form field checks.
- *
- * Custom Events:
- * --------------
- * TexareaResize:resize
- *   Fired when the textarea is resized.
- *   params: NONE
- *
- * @author Michael Slusarz <slusarz@horde.org>
- */
-
-var TextareaResize = Class.create({
-    // Variables used: elt, max_rows, size
-
-    initialize: function(id, opts)
-    {
-        opts = opts || {};
-
-        this.elt = $(id);
-        this.max_rows = opts.max_rows || 5;
-        this.size = -1;
-
-        new Form.Element.Observer(this.elt, opts.observe_time || 1, this.resize.bind(this));
-
-        this.resize();
-    },
-
-    resize: function()
-    {
-        var old_rows, rows,
-            size = $F(this.elt).length;
-
-        if (size == this.size) {
-            return;
-        }
-
-        old_rows = rows = Number(this.elt.readAttribute('rows', 1));
-
-        if (size > this.size) {
-            while (rows < this.max_rows) {
-                if (this.elt.scrollHeight == this.elt.clientHeight) {
-                    break;
-                }
-                this.elt.writeAttribute('rows', ++rows);
-            }
-        } else if (rows > 1) {
-            do {
-                this.elt.writeAttribute('rows', --rows);
-                if (this.elt.scrollHeight != this.elt.clientHeight) {
-                    this.elt.writeAttribute('rows', ++rows);
-                    break;
-                }
-            } while (rows > 1);
-        }
-
-        this.size = size;
-
-        if (rows != old_rows) {
-            this.elt.fire('TextareaResize:resize');
-        }
-    }
-
-});
index aaea2d4..f124717 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * autocomplete.js - A javascript library which implements autocomplete.
  * Requires prototype.js v1.6.0.2+, scriptaculous v1.8.0+ (effects.js),
- * and KeyNavList.js.
+ * and keynavlist.js.
  *
  * Adapted from script.aculo.us controls.js v1.8.0
  *   (c) 2005-2007 Thomas Fuchs, Ivan Krstic, and Jon Tirsen
diff --git a/horde/js/contextsensitive.js b/horde/js/contextsensitive.js
new file mode 100644 (file)
index 0000000..8c76ae9
--- /dev/null
@@ -0,0 +1,388 @@
+/**
+ * ContextSensitive: a library for generating context-sensitive content on
+ * HTML elements. It will take over the click/oncontextmenu functions for the
+ * document, and works only where these are possible to override.  It allows
+ * contextmenus to be created via both a left and right mouse click.
+ *
+ * On Opera, the context menu is triggered by a left click + SHIFT + CTRL
+ * combination.
+ *
+ * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only).
+ *
+ *
+ * Usage:
+ * ------
+ * cs = new ContextSensitive();
+ *
+ * Custom Events:
+ * --------------
+ * Custom events are triggered on the base element. The parameters given
+ * below are available through the 'memo' property of the Event object.
+ *
+ * ContextSensitive:click
+ *   Fired when a contextmenu element is clicked on.
+ *   params: (object) elt - (Element) The menu element clicked on.
+ *                    trigger - (string) The parent menu.
+ *
+ * ContextSensitive:show
+ *   Fired before a contextmenu is displayed.
+ *   params: (string) The DOM ID of the context menu.
+ *
+ *
+ * Original code by Havard Eide (http://eide.org/) released under the MIT
+ * license.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author Michael Slusarz <slusarz@horde.org>
+ */
+
+var ContextSensitive = Class.create({
+
+    initialize: function()
+    {
+        this.baseelt = null;
+        this.current = [];
+        this.elements = $H();
+        this.submenus = $H();
+        this.triggers = [];
+
+        if (!Prototype.Browser.Opera) {
+            document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this));
+        }
+        document.observe('click', this._leftClickHandler.bindAsEventListener(this));
+        document.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousescroll', this.close.bind(this));
+    },
+
+    /**
+     * Elements are of type ContextSensitive.Element.
+     */
+    addElement: function(id, target, opts)
+    {
+        var left = Boolean(opts.left);
+        if (id && !this.validElement(id, left)) {
+            this.elements.set(id + Number(left), new ContextSensitive.Element(id, target, opts));
+        }
+    },
+
+    /**
+     * Remove a registered element.
+     */
+    removeElement: function(id)
+    {
+        this.elements.unset(id + '0');
+        this.elements.unset(id + '1');
+    },
+
+    /**
+     * Hide the currently displayed element(s).
+     */
+    close: function()
+    {
+        this._closeMenu(0, true);
+    },
+
+    /**
+     * Close all menus below a specified level.
+     */
+    _closeMenu: function(idx, immediate)
+    {
+        if (this.current.size()) {
+            this.current.splice(idx, this.current.size() - idx).each(function(s) {
+                // Fade-out on final display.
+                if (!immediate && idx == 0) {
+                    s.fade({ duration: 0.15 });
+                } else {
+                    $(s).hide();
+                }
+            });
+
+            this.triggers.splice(idx, this.triggers.size() - idx).each(function(s) {
+                $(s).removeClassName('contextHover');
+            });
+
+            if (idx == 0) {
+                this.baseelt = null;
+            }
+        }
+    },
+
+    /**
+     * Returns the current displayed menu element ID, if any. If more than one
+     * submenu is open, returns the last ID opened.
+     */
+    currentmenu: function()
+    {
+        return this.current.last();
+    },
+
+    /**
+     * Get a valid element (the ones that can be right-clicked) based
+     * on a element ID.
+     */
+    validElement: function(id, left)
+    {
+        return this.elements.get(id + Number(Boolean(left)));
+    },
+
+    /**
+     * Set the disabled flag of an event.
+     */
+    disable: function(id, left, disable)
+    {
+        var e = this.validElement(id, left);
+        if (e) {
+            e.disable = disable;
+        }
+    },
+
+    /**
+     * Called when a left click event occurs. Will return before the
+     * element is closed if we click on an element inside of it.
+     */
+    _leftClickHandler: function(e)
+    {
+        var base, elt, elt_up, trigger;
+
+        if (this.operaCheck(e)) {
+            this._rightClickHandler(e, false);
+            e.stop();
+            return;
+        }
+
+        // Check for a right click. FF on Linux triggers an onclick event even
+        // w/a right click, so disregard.
+        if (e.isRightClick()) {
+            return;
+        }
+
+        // Check for click in open contextmenu.
+        if (this.current.size()) {
+            elt = e.element();
+            if (!elt.match('A')) {
+                elt = elt.up('A');
+                if (!elt) {
+                    this._rightClickHandler(e, true);
+                    return;
+                }
+            }
+            elt_up = elt.up('.contextMenu');
+
+            if (elt_up) {
+                e.stop();
+
+                if (elt.hasClassName('contextSubmenu') &&
+                    elt_up.identify() != this.currentmenu()) {
+                    this._closeMenu(this.current.indexOf(elt.identify()));
+                }
+
+                base = this.baseelt;
+                trigger = this.triggers.last();
+                this.close();
+                base.fire('ContextSensitive:click', { elt: elt, trigger: trigger });
+                return;
+            }
+        }
+
+        // Check if the mouseclick is registered to an element now.
+        this._rightClickHandler(e, true);
+    },
+
+    /**
+     * Checks if the Opera right-click emulation is present.
+     */
+    operaCheck: function(e)
+    {
+        return Prototype.Browser.Opera && e.shiftKey && e.ctrlKey;
+    },
+
+    /**
+     * Called when a right click event occurs.
+     */
+    _rightClickHandler: function(e, left)
+    {
+        if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) {
+            e.stop();
+        }
+    },
+
+    /**
+     * Display context menu if valid element has been activated.
+     */
+    trigger: function(target, leftclick, x, y)
+    {
+        var ctx, el, offset, offsets, voffsets;
+
+        [ target ].concat(target.ancestors()).find(function(n) {
+            ctx = this.validElement(n.id, leftclick);
+            return ctx;
+        }, this);
+
+        // Try to retrieve the context-sensitive element we want to
+        // display. If we can't find it we just return.
+        if (!ctx ||
+            ctx.disable ||
+            !(el = $(ctx.ctx)) ||
+            (leftclick && target == this.baseelt) ||
+            this.currentmenu() == ctx.ctx) {
+            this.close();
+            return false;
+        }
+
+        this.close();
+
+        // Register the element that was clicked on.
+        this.baseelt = target;
+
+        offset = ctx.opts.offset;
+        if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) {
+            offset = target.identify();
+        }
+        offset = $(offset);
+
+        if (offset) {
+            offsets = offset.viewportOffset();
+            voffsets = document.viewport.getScrollOffsets();
+            x = offsets[0] + voffsets.left;
+            y = offsets[1] + offset.getHeight() + voffsets.top;
+        }
+
+        this._displayMenu(el, x, y);
+        this.triggers.push(el.identify());
+
+        return true;
+    },
+
+    /**
+     * Display the [sub]menu on the screen.
+     */
+    _displayMenu: function(elt, x, y)
+    {
+        // Get window/element dimensions
+        var eltL, h, w,
+            id = elt.identify(),
+            v = document.viewport.getDimensions();
+
+        elt.setStyle({ visibility: 'hidden' }).show();
+        eltL = elt.getLayout(),
+        h = eltL.get('border-box-height');
+        w = eltL.get('border-box-width');
+        elt.hide().setStyle({ visibility: 'visible' });
+
+        // Make sure context window is entirely on screen
+        if ((y + h) > v.height) {
+            y = v.height - h - 2;
+        }
+
+        if ((x + w) > v.width) {
+            x = this.current.size()
+                ? ($(this.current.last()).viewportOffset()[0] - w)
+                : (v.width - w - 2);
+        }
+
+        this.baseelt.fire('ContextSensitive:show', id);
+
+        elt.setStyle({ left: x + 'px', top: y + 'px' })
+
+        if (this.current.size()) {
+            elt.show();
+        } else {
+            // Fade-in on initial display.
+            elt.appear({ duration: 0.15 });
+        }
+
+        this.current.push(id);
+    },
+
+    /**
+     * Add a submenu to an existing menu.
+     */
+    addSubMenu: function(id, submenu)
+    {
+        if (!this.submenus.get(id)) {
+            if (!this.submenus.size()) {
+                document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this));
+            }
+            this.submenus.set(id, submenu);
+            $(submenu).addClassName('contextMenu');
+            $(id).addClassName('contextSubmenu');
+        }
+    },
+
+    /**
+     * Mouseover DOM Event handler.
+     */
+    _mouseoverHandler: function(e)
+    {
+        if (!this.current.size()) {
+            return;
+        }
+
+        var cm = this.currentmenu(),
+            elt = e.element(),
+            elt_up = elt.up('.contextMenu'),
+            id = elt.identify(),
+            id_div, offsets, sub, voffsets, x, y;
+
+        if (!elt_up) {
+            return;
+        }
+
+        id_div = elt_up.identify();
+
+        if (elt.hasClassName('contextSubmenu')) {
+            sub = this.submenus.get(id);
+            if (sub != cm || this.currentmenu() != id) {
+                if (id_div != cm) {
+                    this._closeMenu(this.current.indexOf(id_div) + 1);
+                }
+
+                offsets = elt.viewportOffset();
+                voffsets = document.viewport.getScrollOffsets();
+                x = offsets[0] + voffsets.left + elt.getWidth();
+                y = offsets[1] + voffsets.top;
+                this._displayMenu($(sub), x, y, id);
+                this.triggers.push(id);
+                elt.addClassName('contextHover');
+            }
+        } else if ((this.current.size() > 1) &&
+                   id_div != cm) {
+            this._closeMenu(this.current.indexOf(id));
+        }
+    }
+
+});
+
+ContextSensitive.Element = Class.create({
+
+    // opts: 'left' -> monitor left click; 'offset' -> id of element used to
+    //       determine offset placement
+    initialize: function(id, target, opts)
+    {
+        this.id = id;
+        this.ctx = target;
+        this.opts = opts;
+        this.opts.left = Boolean(opts.left);
+        this.disable = false;
+
+        target = $(target);
+        if (target) {
+            target.addClassName('contextMenu');
+        }
+    }
+
+});
diff --git a/horde/js/dhtmlHistory.js b/horde/js/dhtmlHistory.js
deleted file mode 100644 (file)
index 49681aa..0000000
+++ /dev/null
@@ -1,499 +0,0 @@
-/**
- * dhtmlHistory - An object that provides DHTML history, history data, and
- * bookmarking for AJAX applications.
- *
- * Copyright (c) 2007 Brian Dillard and Brad Neuberg:
- * Brian Dillard | Project Lead | bdillard@pathf.com | http://blogs.pathf.com/agileajax/
- * Brad Neuberg | Original Project Creator | http://codinginparadise.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
- * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
- * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- *
- * This file has been altered from the original dhtmlHistory (v0.05; SVN
- * revision 114) to remove unneeded functionality and to provide bug fixes and
- * enhancements.
- *
- * This file requires the Prototype Javscript Library v1.6.0+
- *
- * Additions Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- */
-
-window.Horde = window.Horde || {};
-
-Horde.dhtmlHistory = {
-    /* Our current hash location, without the "#" symbol. */
-    // currentLocation: null,
-
-    /* Our history change listener. */
-    // listener: null,
-
-    /* A hidden IFrame we use in Internet Explorer to detect history
-       changes. */
-    // iframe: null,
-
-    /* Indicates to the browser whether to ignore location changes. */
-    // ignoreLocationChange: null,
-
-    /* The amount of time in milliseconds that we should wait between add
-       requests. Firefox is okay with 200 ms, but Internet Explorer needs
-       400. */
-    WAIT_TIME: 200,
-
-    /* The amount of time in milliseconds an add request has to wait in line
-       before being run on a setTimeout(). */
-    currentWaitTime: 0,
-
-    /* A flag that indicates that we should fire a history change event when
-       we are ready, i.e. after we are initialized and we have a history
-       change listener. This is needed due to an edge case in browsers other
-       than Internet Explorer; if you leave a page entirely then return, we
-       must fire this as a history change event. Unfortunately, we have lost
-       all references to listeners from earlier, because JavaScript
-       clears out. */
-    // fireOnNewListener: null,
-
-    /* A variable that indicates whether this is the first time this page has
-       been loaded. If you go to a web page, leave it for another one, and
-       then return, the page's onload listener fires again. We need a way to
-       differentiate between the first page load and subsequent ones.  This
-       variable works hand in hand with the pageLoaded variable we store into
-       historyStorage. */
-    // firstLoad: false,
-
-    /* A variable to handle an important edge case in Internet Explorer. In
-       IE, if a user manually types an address into their browser's location
-       bar, we must intercept this by continiously checking the location bar
-       with a timer interval. However, if we manually change the location
-       bar ourselves programmatically, when using our hidden iframe, we need
-       to ignore these changes. Unfortunately, these changes are not atomic,
-       so we surround them with the variable 'ieAtomicLocationChange', that if
-       true means we are programmatically setting the location and should
-       ignore this atomic chunked change. */
-    // ieAtomicLocationChange: null,
-
-    /* Safari only variables. */
-    // safariHistoryStartPoint: null,
-    // safariStack: null,
-
-    /* PeriodicalExecuter instance. */
-    // pe: null,
-
-    /* Initializes our DHTML history. You should call this after the page is
-       finished loading. Returns true on success, false on failure. */
-    initialize: function()
-    {
-        if (navigator.vendor && navigator.vendor === 'KDE') {
-            return false;
-        }
-
-        Horde.historyStorage.init();
-
-        if (Prototype.Browser.WebKit) {
-            this.createSafari();
-        } else if (Prototype.Browser.Opera) {
-            this.createOpera();
-        }
-
-        // Get our initial location
-        this.currentLocation = this.getCurrentLocation();
-
-        // Write out a hidden iframe for IE and set the amount of time to
-        // wait between add() requests.
-        if (Prototype.Browser.IE) {
-            this.iframe = new Element('IFRAME', { frameborder: 0, name: 'DhtmlHistoryFrame', id: 'DhtmlHistoryFrame', src: 'javascript:false;' }).hide();
-            $(document.body).insert(this.iframe);
-            this.writeIframe(this.currentLocation);
-
-            // Wait 400 milliseconds between history updates on IE
-            this.WAIT_TIME = 400;
-
-            this.ignoreLocationChange = true;
-        }
-
-        /* Add an unload listener for the page; this is needed for FF 1.5+
-           because this browser caches all dynamic updates to the page, which
-           can break some of our logic related to testing whether this is the
-           first instance a page has loaded or whether it is being pulled from
-           the cache. */
-        Event.observe(window, 'unload', function() { this.firstLoad = false; }.bind(this));
-
-        this.isFirstLoad();
-
-        /* Other browsers can use a location handler that checks at regular
-           intervals as their primary mechanism; we use it for IE as well to
-           handle an important edge case; see checkLocation() for details. */
-        this.pe = new PeriodicalExecuter(this.checkLocation.bind(this), 0.1);
-
-        return true;
-    },
-
-    stop: function()
-    {
-        if (this.pe) {
-            this.pe.stop();
-        }
-    },
-
-    /* Adds a history change listener. Note that only one listener is
-       supported at this time. */
-    addListener: function(callback)
-    {
-        this.listener = callback;
-
-        /* If the page was just loaded and we should not ignore it, fire an
-           event to our new listener now. */
-        if (this.fireOnNewListener) {
-            if (this.currentLocation) {
-                this.fireHistoryEvent(this.currentLocation);
-            }
-            this.fireOnNewListener = false;
-        }
-    },
-
-    add: function(newLoc, historyData)
-    {
-        if (Prototype.Browser.WebKit) {
-            newLoc = this.removeHash(newLoc);
-            Horde.historyStorage.put(newLoc, historyData);
-            this.currentLocation = newLoc;
-            this.ignoreLocationChange = true;
-            this.setLocation(newLoc);
-            this.putSafariState(newLoc);
-        } else {
-            /* Most browsers require that we wait a certain amount of time
-               before changing the location, such as 200 milliseconds; rather
-               than forcing external callers to use setTimeout() to account for
-               this to prevent bugs, we internally handle this detail by using
-               a 'currentWaitTime' variable and have requests wait in line. */
-            setTimeout(this.addImpl.bind(this, newLoc, historyData), this.currentWaitTime);
-        }
-
-        // Indicate that the next request will have to wait for awhile
-        this.currentWaitTime += this.WAIT_TIME;
-    },
-
-    setLocation: function(loc)
-    {
-        location.hash = loc;
-    },
-
-    /* Gets the current hash value that is in the browser's location bar,
-       removing leading # symbols if they are present. */
-    getCurrentLocation: function()
-    {
-        if (Prototype.Browser.WebKit) {
-            return this.getSafariState();
-        } else {
-            return this.removeHash(decodeURIComponent(location.hash));
-        }
-    },
-
-    addImpl: function(newLoc, historyData)
-    {
-        // Indicate that the current wait time is now less
-        if (this.currentWaitTime) {
-            this.currentWaitTime -= this.WAIT_TIME;
-        }
-
-        /* IE has a strange bug; if the newLoc is the same as _any_
-           preexisting id in the document, then the history action gets
-           recorded twice; return immediately if there is an element with
-           this ID. */
-        if ($('newLoc')) {
-            return;
-        }
-
-        // Remove any leading hash symbols on newLoc
-        newLoc = this.removeHash(newLoc);
-
-        // Store the history data into history storage
-        Horde.historyStorage.put(newLoc, historyData);
-
-        // Indicate to the browser to ignore this upcoming location change.
-        // Indicate to IE that this is an atomic location change block.
-        this.ignoreLocationChange = this.ieAtomicLocationChange = true;
-
-        // Save this as our current location and change the browser location
-        this.currentLocation = newLoc;
-        this.setLocation(encodeURIComponent(newLoc));
-
-        // Change the hidden iframe's location if on IE
-        if (Prototype.Browser.IE) {
-            this.writeIframe(newLoc);
-        }
-
-        // End of atomic location change block for IE
-        this.ieAtomicLocationChange = false;
-    },
-
-    isFirstLoad: function()
-    {
-        if (!Horde.historyStorage.hasKey("DhtmlHistory_pageLoaded")) {
-            if (Prototype.Browser.IE) {
-                this.fireOnNewListener = false;
-            } else {
-                this.ignoreLocationChange = true;
-            }
-            this.firstLoad = true;
-            Horde.historyStorage.put("DhtmlHistory_pageLoaded", true);
-        } else {
-            if (Prototype.Browser.IE) {
-                this.firstLoad = false;
-            } else {
-                /* Indicate that we want to pay attention to this location
-                   change. */
-                this.ignoreLocationChange = false;
-            }
-
-            /* For browsers other than IE, fire a history change event;
-               on IE, the event will be thrown automatically when it's
-               hidden iframe reloads on page load. Unfortunately, we don't
-               have any listeners yet; indicate that we want to fire an
-               event when a listener is added. */
-            this.fireOnNewListener = true;
-        }
-    },
-
-    /* Notify the listener of new history changes. */
-    fireHistoryEvent: function(newHash)
-    {
-        if (this.listener) {
-            // Extract the value from our history storage for this hash and
-            // call our listener.
-            this.listener.call(null, newHash, Horde.historyStorage.get(newHash));
-        }
-    },
-
-    /* See if the browser has changed location.  This is the primary history
-       mechanism for FF. For IE, we use this to handle an important edge case:
-       if a user manually types in a new hash value into their IE location
-       bar and press enter, we want to intercept this and notify any history
-       listener. */
-    checkLocation: function()
-    {
-        /* Ignore any location changes that we made ourselves for browsers
-           other than IE. */
-        if (!Prototype.Browser.IE) {
-            if (this.ignoreLocationChange) {
-                this.ignoreLocationChange = false;
-                return;
-            }
-        } else if (this.ieAtomicLocationChange) {
-            /* If we are dealing with IE and we are in the middle of making a
-               location change from an iframe, ignore it. */
-            return;
-        }
-
-        // Get hash location
-        var hash = this.getCurrentLocation();
-
-        // See if there has been a change or there is no hash location
-        if (hash.replace(/@/, '%40') == this.currentLocation || Object.isUndefined(hash)) {
-            return;
-        }
-
-        /* On IE, we need to intercept users manually entering locations into
-           the browser; we do this by comparing the browsers location against
-           the iframes location; if they differ, we are dealing with a manual
-           event and need to place it inside our history, otherwise we can
-           return. */
-        this.ieAtomicLocationChange = true;
-
-        if (Prototype.Browser.IE) {
-            if (this.iframe.contentWindow.l == hash) {
-                // The iframe is unchanged
-                return;
-            }
-            this.writeIframe(hash);
-        }
-
-        // Save this new location
-        this.currentLocation = hash;
-
-        this.ieAtomicLocationChange = false;
-
-        // Notify listeners of the change
-        this.fireHistoryEvent(hash);
-    },
-
-    /* Removes any leading hash that might be on a location. */
-    removeHash: function(h)
-    {
-        if (h === null || Object.isUndefined(h)) {
-            return null;
-        } else if (h.startsWith('#')) {
-            if (h.length == 1) {
-                return "";
-            } else {
-                return h.substring(1);
-            }
-        }
-        return h;
-    },
-
-    // IE Specific Code
-    /* For IE, says when the hidden iframe has finished loading. */
-    iframeLoaded: function(newLoc)
-    {
-        // Ignore any location changes that we made ourselves
-        if (this.ignoreLocationChange) {
-            this.ignoreLocationChange = false;
-            return;
-        }
-
-        // Get the new location
-        this.setLocation(encodeURIComponent(newLoc));
-
-        // Notify listeners of the change
-        this.fireHistoryEvent(newLoc);
-    },
-
-    writeIframe: function(l)
-    {
-        var d = this.iframe.contentWindow.document;
-        d.open();
-        d.write('<html><script type="text/javascript">var l="' + l + '";function pageLoaded(){window.parent.Horde.dhtmlHistory.iframeLoaded(l);}</script><body onload="pageLoaded()"></body></html>');
-        d.close();
-    },
-
-    // Safari specific code
-    createSafari: function()
-    {
-        this.WAIT_TIME = 400;
-        this.safariHistoryStartPoint = history.length;
-
-        this.safariStack = new Element('INPUT', { id: 'DhtmlSafariHistory', type: 'text', value: '[]' }).hide();
-        $(document.body).insert(this.safariStack);
-    },
-
-    getSafariStack: function()
-    {
-        return $F(this.safariStack).evalJSON();
-    },
-
-    getSafariState: function()
-    {
-        var stack = this.getSafariStack();
-        return stack[history.length - this.safariHistoryStartPoint - 1];
-    },
-
-    putSafariState: function(newLoc)
-    {
-        var stack = this.getSafariStack();
-        stack[history.length - this.safariHistoryStartPoint] = newLoc;
-        this.safariStack.setValue(stack.toJSON());
-    },
-
-    // Opera specific code
-    createOpera: function()
-    {
-        this.WAIT_TIME = 400;
-        $(document.body).insert(new Element('IMG', { src: "javascript:location.href='javascript:Horde.dhtmlHistory.checkLocation();'" }).hide());
-    }
-};
-
-/* An object that uses a hidden form to store history state across page loads.
-   The chief mechanism for doing so is using the fact that browsers save the
-   text in form data for the life of the browser and cache, which means the
-   text is still there when the user navigates back to the page. See
-   http://codinginparadise.org/weblog/2005/08/ajax-tutorial-saving-session-across.html
-   for full details. */
-Horde.historyStorage = {
-    /* Our hash of key name/values. */
-    // storageHash: null,
-
-    /* A reference to our textarea field. */
-    // storageField: null,
-
-    put: function(key, value)
-    {
-        this.loadHashTable();
-
-        // Store this new key
-        this.storageHash.set(key, value);
-
-        // Save and serialize the hashtable into the form
-        this.saveHashTable();
-    },
-
-    get: function(key)
-    {
-        // Make sure the hash table has been loaded from the form
-        this.loadHashTable();
-
-        var value = this.storageHash.get(key);
-        return Object.isUndefined(value) ? null : value;
-    },
-
-    remove: function(key)
-    {
-        // Make sure the hash table has been loaded from the form
-        this.loadHashTable();
-
-        // Delete the value
-        this.storageHash.unset(key);
-
-        // Serialize and save the hash table into the form
-        this.saveHashTable();
-    },
-
-    /* Clears out all saved data. */
-    reset: function()
-    {
-        this.storageField.value = "";
-        this.storageHash = $H();
-    },
-
-    hasKey: function(key)
-    {
-        // Make sure the hash table has been loaded from the form
-        this.loadHashTable();
-        return !(typeof this.storageHash.get(key) == undefined);
-    },
-
-    init: function()
-    {
-        // Write a hidden form into the page
-        var form = new Element('FORM').hide();
-        $(document.body).insert(form);
-
-        this.storageField = new Element('TEXTAREA', { id: 'historyStorageField' });
-        form.insert(this.storageField);
-
-        if (Prototype.Browser.Opera) {
-            this.storageField.focus();
-        }
-    },
-
-    /* Loads the hash table up from the form. */
-    loadHashTable: function()
-    {
-        if (!this.storageHash) {
-            // Destringify the content back into a real JS object
-            this.storageHash = (this.storageField.value) ? this.storageField.value.evalJSON() : $H();
-        }
-    },
-
-    /* Saves the hash table into the form. */
-    saveHashTable: function()
-    {
-        this.loadHashTable();
-        this.storageField.value = this.storageHash.toJSON();
-    }
-
-};
diff --git a/horde/js/dhtmlhistory.js b/horde/js/dhtmlhistory.js
new file mode 100644 (file)
index 0000000..49681aa
--- /dev/null
@@ -0,0 +1,499 @@
+/**
+ * dhtmlHistory - An object that provides DHTML history, history data, and
+ * bookmarking for AJAX applications.
+ *
+ * Copyright (c) 2007 Brian Dillard and Brad Neuberg:
+ * Brian Dillard | Project Lead | bdillard@pathf.com | http://blogs.pathf.com/agileajax/
+ * Brad Neuberg | Original Project Creator | http://codinginparadise.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+ * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * This file has been altered from the original dhtmlHistory (v0.05; SVN
+ * revision 114) to remove unneeded functionality and to provide bug fixes and
+ * enhancements.
+ *
+ * This file requires the Prototype Javscript Library v1.6.0+
+ *
+ * Additions Copyright 2005-2010 The Horde Project (http://www.horde.org/)
+ */
+
+window.Horde = window.Horde || {};
+
+Horde.dhtmlHistory = {
+    /* Our current hash location, without the "#" symbol. */
+    // currentLocation: null,
+
+    /* Our history change listener. */
+    // listener: null,
+
+    /* A hidden IFrame we use in Internet Explorer to detect history
+       changes. */
+    // iframe: null,
+
+    /* Indicates to the browser whether to ignore location changes. */
+    // ignoreLocationChange: null,
+
+    /* The amount of time in milliseconds that we should wait between add
+       requests. Firefox is okay with 200 ms, but Internet Explorer needs
+       400. */
+    WAIT_TIME: 200,
+
+    /* The amount of time in milliseconds an add request has to wait in line
+       before being run on a setTimeout(). */
+    currentWaitTime: 0,
+
+    /* A flag that indicates that we should fire a history change event when
+       we are ready, i.e. after we are initialized and we have a history
+       change listener. This is needed due to an edge case in browsers other
+       than Internet Explorer; if you leave a page entirely then return, we
+       must fire this as a history change event. Unfortunately, we have lost
+       all references to listeners from earlier, because JavaScript
+       clears out. */
+    // fireOnNewListener: null,
+
+    /* A variable that indicates whether this is the first time this page has
+       been loaded. If you go to a web page, leave it for another one, and
+       then return, the page's onload listener fires again. We need a way to
+       differentiate between the first page load and subsequent ones.  This
+       variable works hand in hand with the pageLoaded variable we store into
+       historyStorage. */
+    // firstLoad: false,
+
+    /* A variable to handle an important edge case in Internet Explorer. In
+       IE, if a user manually types an address into their browser's location
+       bar, we must intercept this by continiously checking the location bar
+       with a timer interval. However, if we manually change the location
+       bar ourselves programmatically, when using our hidden iframe, we need
+       to ignore these changes. Unfortunately, these changes are not atomic,
+       so we surround them with the variable 'ieAtomicLocationChange', that if
+       true means we are programmatically setting the location and should
+       ignore this atomic chunked change. */
+    // ieAtomicLocationChange: null,
+
+    /* Safari only variables. */
+    // safariHistoryStartPoint: null,
+    // safariStack: null,
+
+    /* PeriodicalExecuter instance. */
+    // pe: null,
+
+    /* Initializes our DHTML history. You should call this after the page is
+       finished loading. Returns true on success, false on failure. */
+    initialize: function()
+    {
+        if (navigator.vendor && navigator.vendor === 'KDE') {
+            return false;
+        }
+
+        Horde.historyStorage.init();
+
+        if (Prototype.Browser.WebKit) {
+            this.createSafari();
+        } else if (Prototype.Browser.Opera) {
+            this.createOpera();
+        }
+
+        // Get our initial location
+        this.currentLocation = this.getCurrentLocation();
+
+        // Write out a hidden iframe for IE and set the amount of time to
+        // wait between add() requests.
+        if (Prototype.Browser.IE) {
+            this.iframe = new Element('IFRAME', { frameborder: 0, name: 'DhtmlHistoryFrame', id: 'DhtmlHistoryFrame', src: 'javascript:false;' }).hide();
+            $(document.body).insert(this.iframe);
+            this.writeIframe(this.currentLocation);
+
+            // Wait 400 milliseconds between history updates on IE
+            this.WAIT_TIME = 400;
+
+            this.ignoreLocationChange = true;
+        }
+
+        /* Add an unload listener for the page; this is needed for FF 1.5+
+           because this browser caches all dynamic updates to the page, which
+           can break some of our logic related to testing whether this is the
+           first instance a page has loaded or whether it is being pulled from
+           the cache. */
+        Event.observe(window, 'unload', function() { this.firstLoad = false; }.bind(this));
+
+        this.isFirstLoad();
+
+        /* Other browsers can use a location handler that checks at regular
+           intervals as their primary mechanism; we use it for IE as well to
+           handle an important edge case; see checkLocation() for details. */
+        this.pe = new PeriodicalExecuter(this.checkLocation.bind(this), 0.1);
+
+        return true;
+    },
+
+    stop: function()
+    {
+        if (this.pe) {
+            this.pe.stop();
+        }
+    },
+
+    /* Adds a history change listener. Note that only one listener is
+       supported at this time. */
+    addListener: function(callback)
+    {
+        this.listener = callback;
+
+        /* If the page was just loaded and we should not ignore it, fire an
+           event to our new listener now. */
+        if (this.fireOnNewListener) {
+            if (this.currentLocation) {
+                this.fireHistoryEvent(this.currentLocation);
+            }
+            this.fireOnNewListener = false;
+        }
+    },
+
+    add: function(newLoc, historyData)
+    {
+        if (Prototype.Browser.WebKit) {
+            newLoc = this.removeHash(newLoc);
+            Horde.historyStorage.put(newLoc, historyData);
+            this.currentLocation = newLoc;
+            this.ignoreLocationChange = true;
+            this.setLocation(newLoc);
+            this.putSafariState(newLoc);
+        } else {
+            /* Most browsers require that we wait a certain amount of time
+               before changing the location, such as 200 milliseconds; rather
+               than forcing external callers to use setTimeout() to account for
+               this to prevent bugs, we internally handle this detail by using
+               a 'currentWaitTime' variable and have requests wait in line. */
+            setTimeout(this.addImpl.bind(this, newLoc, historyData), this.currentWaitTime);
+        }
+
+        // Indicate that the next request will have to wait for awhile
+        this.currentWaitTime += this.WAIT_TIME;
+    },
+
+    setLocation: function(loc)
+    {
+        location.hash = loc;
+    },
+
+    /* Gets the current hash value that is in the browser's location bar,
+       removing leading # symbols if they are present. */
+    getCurrentLocation: function()
+    {
+        if (Prototype.Browser.WebKit) {
+            return this.getSafariState();
+        } else {
+            return this.removeHash(decodeURIComponent(location.hash));
+        }
+    },
+
+    addImpl: function(newLoc, historyData)
+    {
+        // Indicate that the current wait time is now less
+        if (this.currentWaitTime) {
+            this.currentWaitTime -= this.WAIT_TIME;
+        }
+
+        /* IE has a strange bug; if the newLoc is the same as _any_
+           preexisting id in the document, then the history action gets
+           recorded twice; return immediately if there is an element with
+           this ID. */
+        if ($('newLoc')) {
+            return;
+        }
+
+        // Remove any leading hash symbols on newLoc
+        newLoc = this.removeHash(newLoc);
+
+        // Store the history data into history storage
+        Horde.historyStorage.put(newLoc, historyData);
+
+        // Indicate to the browser to ignore this upcoming location change.
+        // Indicate to IE that this is an atomic location change block.
+        this.ignoreLocationChange = this.ieAtomicLocationChange = true;
+
+        // Save this as our current location and change the browser location
+        this.currentLocation = newLoc;
+        this.setLocation(encodeURIComponent(newLoc));
+
+        // Change the hidden iframe's location if on IE
+        if (Prototype.Browser.IE) {
+            this.writeIframe(newLoc);
+        }
+
+        // End of atomic location change block for IE
+        this.ieAtomicLocationChange = false;
+    },
+
+    isFirstLoad: function()
+    {
+        if (!Horde.historyStorage.hasKey("DhtmlHistory_pageLoaded")) {
+            if (Prototype.Browser.IE) {
+                this.fireOnNewListener = false;
+            } else {
+                this.ignoreLocationChange = true;
+            }
+            this.firstLoad = true;
+            Horde.historyStorage.put("DhtmlHistory_pageLoaded", true);
+        } else {
+            if (Prototype.Browser.IE) {
+                this.firstLoad = false;
+            } else {
+                /* Indicate that we want to pay attention to this location
+                   change. */
+                this.ignoreLocationChange = false;
+            }
+
+            /* For browsers other than IE, fire a history change event;
+               on IE, the event will be thrown automatically when it's
+               hidden iframe reloads on page load. Unfortunately, we don't
+               have any listeners yet; indicate that we want to fire an
+               event when a listener is added. */
+            this.fireOnNewListener = true;
+        }
+    },
+
+    /* Notify the listener of new history changes. */
+    fireHistoryEvent: function(newHash)
+    {
+        if (this.listener) {
+            // Extract the value from our history storage for this hash and
+            // call our listener.
+            this.listener.call(null, newHash, Horde.historyStorage.get(newHash));
+        }
+    },
+
+    /* See if the browser has changed location.  This is the primary history
+       mechanism for FF. For IE, we use this to handle an important edge case:
+       if a user manually types in a new hash value into their IE location
+       bar and press enter, we want to intercept this and notify any history
+       listener. */
+    checkLocation: function()
+    {
+        /* Ignore any location changes that we made ourselves for browsers
+           other than IE. */
+        if (!Prototype.Browser.IE) {
+            if (this.ignoreLocationChange) {
+                this.ignoreLocationChange = false;
+                return;
+            }
+        } else if (this.ieAtomicLocationChange) {
+            /* If we are dealing with IE and we are in the middle of making a
+               location change from an iframe, ignore it. */
+            return;
+        }
+
+        // Get hash location
+        var hash = this.getCurrentLocation();
+
+        // See if there has been a change or there is no hash location
+        if (hash.replace(/@/, '%40') == this.currentLocation || Object.isUndefined(hash)) {
+            return;
+        }
+
+        /* On IE, we need to intercept users manually entering locations into
+           the browser; we do this by comparing the browsers location against
+           the iframes location; if they differ, we are dealing with a manual
+           event and need to place it inside our history, otherwise we can
+           return. */
+        this.ieAtomicLocationChange = true;
+
+        if (Prototype.Browser.IE) {
+            if (this.iframe.contentWindow.l == hash) {
+                // The iframe is unchanged
+                return;
+            }
+            this.writeIframe(hash);
+        }
+
+        // Save this new location
+        this.currentLocation = hash;
+
+        this.ieAtomicLocationChange = false;
+
+        // Notify listeners of the change
+        this.fireHistoryEvent(hash);
+    },
+
+    /* Removes any leading hash that might be on a location. */
+    removeHash: function(h)
+    {
+        if (h === null || Object.isUndefined(h)) {
+            return null;
+        } else if (h.startsWith('#')) {
+            if (h.length == 1) {
+                return "";
+            } else {
+                return h.substring(1);
+            }
+        }
+        return h;
+    },
+
+    // IE Specific Code
+    /* For IE, says when the hidden iframe has finished loading. */
+    iframeLoaded: function(newLoc)
+    {
+        // Ignore any location changes that we made ourselves
+        if (this.ignoreLocationChange) {
+            this.ignoreLocationChange = false;
+            return;
+        }
+
+        // Get the new location
+        this.setLocation(encodeURIComponent(newLoc));
+
+        // Notify listeners of the change
+        this.fireHistoryEvent(newLoc);
+    },
+
+    writeIframe: function(l)
+    {
+        var d = this.iframe.contentWindow.document;
+        d.open();
+        d.write('<html><script type="text/javascript">var l="' + l + '";function pageLoaded(){window.parent.Horde.dhtmlHistory.iframeLoaded(l);}</script><body onload="pageLoaded()"></body></html>');
+        d.close();
+    },
+
+    // Safari specific code
+    createSafari: function()
+    {
+        this.WAIT_TIME = 400;
+        this.safariHistoryStartPoint = history.length;
+
+        this.safariStack = new Element('INPUT', { id: 'DhtmlSafariHistory', type: 'text', value: '[]' }).hide();
+        $(document.body).insert(this.safariStack);
+    },
+
+    getSafariStack: function()
+    {
+        return $F(this.safariStack).evalJSON();
+    },
+
+    getSafariState: function()
+    {
+        var stack = this.getSafariStack();
+        return stack[history.length - this.safariHistoryStartPoint - 1];
+    },
+
+    putSafariState: function(newLoc)
+    {
+        var stack = this.getSafariStack();
+        stack[history.length - this.safariHistoryStartPoint] = newLoc;
+        this.safariStack.setValue(stack.toJSON());
+    },
+
+    // Opera specific code
+    createOpera: function()
+    {
+        this.WAIT_TIME = 400;
+        $(document.body).insert(new Element('IMG', { src: "javascript:location.href='javascript:Horde.dhtmlHistory.checkLocation();'" }).hide());
+    }
+};
+
+/* An object that uses a hidden form to store history state across page loads.
+   The chief mechanism for doing so is using the fact that browsers save the
+   text in form data for the life of the browser and cache, which means the
+   text is still there when the user navigates back to the page. See
+   http://codinginparadise.org/weblog/2005/08/ajax-tutorial-saving-session-across.html
+   for full details. */
+Horde.historyStorage = {
+    /* Our hash of key name/values. */
+    // storageHash: null,
+
+    /* A reference to our textarea field. */
+    // storageField: null,
+
+    put: function(key, value)
+    {
+        this.loadHashTable();
+
+        // Store this new key
+        this.storageHash.set(key, value);
+
+        // Save and serialize the hashtable into the form
+        this.saveHashTable();
+    },
+
+    get: function(key)
+    {
+        // Make sure the hash table has been loaded from the form
+        this.loadHashTable();
+
+        var value = this.storageHash.get(key);
+        return Object.isUndefined(value) ? null : value;
+    },
+
+    remove: function(key)
+    {
+        // Make sure the hash table has been loaded from the form
+        this.loadHashTable();
+
+        // Delete the value
+        this.storageHash.unset(key);
+
+        // Serialize and save the hash table into the form
+        this.saveHashTable();
+    },
+
+    /* Clears out all saved data. */
+    reset: function()
+    {
+        this.storageField.value = "";
+        this.storageHash = $H();
+    },
+
+    hasKey: function(key)
+    {
+        // Make sure the hash table has been loaded from the form
+        this.loadHashTable();
+        return !(typeof this.storageHash.get(key) == undefined);
+    },
+
+    init: function()
+    {
+        // Write a hidden form into the page
+        var form = new Element('FORM').hide();
+        $(document.body).insert(form);
+
+        this.storageField = new Element('TEXTAREA', { id: 'historyStorageField' });
+        form.insert(this.storageField);
+
+        if (Prototype.Browser.Opera) {
+            this.storageField.focus();
+        }
+    },
+
+    /* Loads the hash table up from the form. */
+    loadHashTable: function()
+    {
+        if (!this.storageHash) {
+            // Destringify the content back into a real JS object
+            this.storageHash = (this.storageField.value) ? this.storageField.value.evalJSON() : $H();
+        }
+    },
+
+    /* Saves the hash table into the form. */
+    saveHashTable: function()
+    {
+        this.loadHashTable();
+        this.storageField.value = this.storageHash.toJSON();
+    }
+
+};
diff --git a/horde/js/growler.js b/horde/js/growler.js
new file mode 100644 (file)
index 0000000..02ace41
--- /dev/null
@@ -0,0 +1,283 @@
+/**
+ * Growler.js - Display 'Growl'-like notifications.
+ *
+ * Notice Options (passed to 'Growler.growl()'):
+ * 'className' - (string) An optional additional CSS class to apply to notice.
+ * 'header' - (string) The title that is displayed for the notice.
+ * 'life' - (float) The number of seconds in which the notice remains visible.
+ * 'log' - (boolean) If true, will log the entry.
+ * 'speedin' - (float) The speed in seconds in which the notice is shown.
+ * 'speedout' - (float) The speed in seconds in which the notice is hidden.
+ * 'sticky' - (boolean) Determines if the notice should always remain visible
+ *            until closed by the user.
+ *
+ * Growler Options (passed to 'new Growler()'):
+ * 'location' - (string) The location of the growler notices. This can be:
+ *   tr (top-right)
+ *   br (bottom-right)
+ *   tl (top-left)
+ *   bl (bottom-left)
+ *   tc (top-center)
+ *   bc (bottom-center)
+ * 'log' - (boolean) Enable logging.
+ * 'noalerts' - (string) The localized string to display when no log entries
+ *              are present.
+ *
+ * Custom Events:
+ * --------------
+ * Custom events are triggered on the notice element. The parameters given
+ * below are available through the 'memo' property of the Event object.
+ *
+ * Growler:created
+ *   Fired on TODO
+ *   params: NONE
+ *
+ * Growler:destroyed
+ *   Fired on TODO
+ *   params: NONE
+ *
+ *
+ * Growler has been tested with Safari 3(Mac|Win), Firefox 3(Mac|Win), IE6,
+ * IE7, and Opera.
+ *
+ * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only).
+ *
+ * Code adapted from k.Growler v1.0.0
+ *   http://code.google.com/p/kproto/
+ *   Written by Kevin Armstrong <kevin@kevinandre.com>
+ *   Released under the MIT license
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ */
+
+(function() {
+
+    var noticeOptions = {
+        header: '',
+        speedin: 0.3,
+        speedout: 0.5,
+        life: 5,
+        sticky: false,
+        className: ''
+    },
+
+    growlerOptions = {
+        location: 'tr',
+        log: false,
+        noalerts: 'No Alerts'
+    },
+
+    IE6 = Prototype.Browser.IE
+        ? (parseFloat(navigator.appVersion.split("MSIE ")[1]) || 0) == 6
+        : 0;
+
+    function removeNotice(n, o)
+    {
+        o = o || noticeOptions;
+
+        $(n).fade({
+            duration: o.speedout,
+            afterFinish: function() {
+                try {
+                    var ne = n.down('DIV.GrowlerNoticeExit');
+                    if (!Object.isUndefined(ne)) {
+                        ne.stopObserving('click', removeNotice);
+                    }
+                    n.fire('Growler:destroyed');
+                } catch (e) {}
+
+                try {
+                    n.remove();
+                    if (!$('Growler').childElements().size()) {
+                        $('Growler').hide().setOpacity(1);
+                    }
+                } catch (e) {}
+            }
+        });
+    }
+
+    function removeLog(l)
+    {
+        try {
+            var le = l.down('DIV.GrowlerNoticeExit');
+            if (!Object.isUndefined(le)) {
+                le.stopObserving('click', removeLog);
+            }
+        } catch (e) {}
+        try {
+            l.remove();
+        } catch (e) {}
+    }
+
+    window.Growler = Class.create({
+
+        initialize: function(opts)
+        {
+            var ch, cw, sl, st;
+            opts = Object.extend(Object.clone(growlerOptions), opts || {});
+
+            this.growler = new Element('DIV', { id: 'Growler' }).setStyle({ position: IE6 ? 'absolute' : 'fixed', padding: '10px', zIndex: 50000 }).hide();
+
+            if (IE6) {
+                ch = '0 - this.offsetHeight + ( document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight )';
+                cw = '0 - this.offsetWidth + ( document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth )';
+                sl = '( document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft )';
+                st = '( document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )';
+            } else if (opts.log) {
+                this.growlerlog = new Element('DIV', { id: 'GrowlerLog' }).insert(new Element('DIV').hide().insert(new Element('UL').insert(new Element('LI', { className: 'NoAlerts' }).insert(opts.noalerts))));
+                $(document.body).insert(this.growlerlog);
+            }
+
+            switch (opts.location) {
+            case 'br':
+                if (IE6) {
+                    this.growler.style.setExpression('left', "( " + cw + " + " + sl + " ) + 'px'");
+                    this.growler.style.setExpression('top', "( " + ch + "+ " + st + " ) + 'px'");
+                } else {
+                    this.growler.setStyle({ bottom: 0, right: 0 });
+                }
+                break;
+
+            case 'tl':
+                if (IE6) {
+                    this.growler.style.setExpression('left', sl + " + 'px'");
+                    this.growler.style.setExpression('top', st + " + 'px'");
+                } else {
+                    this.growler.setStyle({ top: 0, left: 0 });
+                }
+                break;
+
+            case 'bl':
+                if (IE6) {
+                    this.growler.style.setExpression('left', sl + " + 'px'");
+                    this.growler.style.setExpression('top', "( " + ch + " + " + st + " ) + 'px'");
+                } else {
+                    this.growler.setStyle({ top: 0, right: 0 });
+                }
+                break;
+
+            case 'tc':
+                if (!IE6) {
+                    this.growler.setStyle({ top: 0, left: '25%', width: '50%' });
+                }
+                break;
+
+            case 'bc':
+                if (!IE6) {
+                    this.growler.setStyle({ bottom: 0, left: '25%', width: '50%' });
+                }
+                break;
+
+            default:
+                if (IE6) {
+                    this.growler.setStyle({ bottom: 'auto', right: 'auto' });
+                    this.growler.style.setExpression('left', "( " + cw + " + " + sl + " ) + 'px'");
+                    this.growler.style.setExpression('top', st + " + 'px'");
+                } else {
+                    this.growler.setStyle({ top: 0, right: 0 });
+                }
+                break;
+            }
+
+            this.growler.wrap(document.body);
+
+            this.growler.observe('mouseenter', function() {
+                this.growler.fade({
+                    duration: 0.25,
+                    queue: { limit: 2, scope: 'growler' },
+                    to: 0.3
+                });
+            }.bind(this));
+            this.growler.observe('mouseleave', function() {
+                this.growler.appear({
+                    duration: 0.25,
+                    queue: { limit: 2, scope: 'growler' },
+                    to: 1
+                });
+            }.bind(this));
+        },
+
+        growl: function(msg, options)
+        {
+            options = options || {};
+            var notice, noticeExit, log, logExit, tmp,
+                opts = Object.clone(noticeOptions);
+            Object.extend(opts, options);
+
+            if (opts.log && this.growlerlog) {
+                tmp = this.growlerlog.down('DIV UL');
+                if (tmp.down().hasClassName('NoAlerts')) {
+                    tmp.down().remove();
+                }
+                log = new Element('LI', { className: opts.className.empty() ? null : opts.className }).insert(msg).insert(new Element('SPAN', { className: 'alertdate'} ).insert('[' + (new Date).toLocaleString() + ']'));
+                logExit = new Element('DIV', { className: 'GrowlerNoticeExit' }).update("&times;");
+                logExit.observe('click', removeLog.curry(log));
+                log.insert(logExit);
+                tmp.insert(log);
+            }
+
+            notice = new Element('DIV', { className: 'GrowlerNotice' }).setStyle({ display: 'block', opacity: 0 });
+            if (!opts.className.empty()) {
+                notice.addClassName(opts.className);
+            }
+
+            noticeExit = new Element('DIV', { className: 'GrowlerNoticeExit' }).update("&times;");
+            noticeExit.observe('click', removeNotice.curry(notice, opts));
+            notice.insert(noticeExit);
+
+            if (!opts.header.empty()) {
+                notice.insert(new Element('DIV', { className: 'GrowlerNoticeHead' }).update(opts.header))
+            }
+
+            notice.insert(new Element('DIV', { className: 'GrowlerNoticeBody' }).update(msg));
+
+            this.growler.show().insert(notice);
+
+            new Effect.Opacity(notice, { to: 0.85, duration: opts.speedin });
+
+            if (!opts.sticky) {
+                removeNotice.delay(opts.life, notice, opts);
+            }
+
+            notice.fire('Growler:created');
+
+            return notice;
+        },
+
+        ungrowl: function(n, o)
+        {
+            removeNotice(n, o);
+        },
+
+        toggleLog: function()
+        {
+            if (!this.growlerlog) {
+                return;
+            }
+            Effect.toggle(this.growlerlog.down('DIV'), 'blind', {
+                duration: 0.5,
+                queue: {
+                    position: 'end',
+                    scope: 'GrowlerLog',
+                    limit: 2
+                }
+            });
+            this.logvisible = !this.logvisible;
+            return this.logvisible;
+        },
+
+        logVisible: function()
+        {
+            return this.growlerlog && this.logvisible;
+        },
+
+        logSize: function()
+        {
+            return (this.growlerlog && this.growlerlog.down('.NoAlerts'))
+                ? 0
+                : this.growlerlog.down('UL').childElements().size();
+        }
+
+    });
+
+})();
diff --git a/horde/js/ieEscGuard.js b/horde/js/ieEscGuard.js
deleted file mode 100644 (file)
index 3fa5ad2..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Javascript code for attaching an onkeydown listener to textarea and
- * text input elements to prevent loss of data when the user hits the
- * ESC key.
- *
- * Requires prototypejs 1.6.0.2+.
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- */
-
-/* Finds all text inputs (input type="text") and textarea tags, and
- * attaches the onkeydown listener to them to avoid ESC clearing the
- * text. */
-if (Prototype.Browser.IE) {
-    document.observe('dom:loaded', function() {
-        [ $$('TEXTAREA'), $$('INPUT[type="text"]') ].flatten().each(function(t) {
-            t.observe('keydown', function(e) { return e.keyCode != 27; });
-        });
-    });
-}
diff --git a/horde/js/ieescguard.js b/horde/js/ieescguard.js
new file mode 100644 (file)
index 0000000..3fa5ad2
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * Javascript code for attaching an onkeydown listener to textarea and
+ * text input elements to prevent loss of data when the user hits the
+ * ESC key.
+ *
+ * Requires prototypejs 1.6.0.2+.
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+/* Finds all text inputs (input type="text") and textarea tags, and
+ * attaches the onkeydown listener to them to avoid ESC clearing the
+ * text. */
+if (Prototype.Browser.IE) {
+    document.observe('dom:loaded', function() {
+        [ $$('TEXTAREA'), $$('INPUT[type="text"]') ].flatten().each(function(t) {
+            t.observe('keydown', function(e) { return e.keyCode != 27; });
+        });
+    });
+}
diff --git a/horde/js/keynavlist.js b/horde/js/keynavlist.js
new file mode 100644 (file)
index 0000000..58c4c27
--- /dev/null
@@ -0,0 +1,288 @@
+/**
+ * Reuseable keyboard or mouse driven list component. Based on
+ * Scriptaculous' AutoCompleter.
+ *
+ * Requires: prototype.js (v1.6.1+)
+ *
+ * Usage:
+ * ======
+ * var knl = new KeyNavList(base, {
+ *     'esc' - (boolean) Escape the displayed output?
+ *     'list' - (array) Array of objects with the following keys:
+ *                      'l' - (label) Display data
+ *                      's' - (selected) True if this entry should be selected
+ *                            by default
+ *                      'v' - (value) Value of entry
+ *     'onChoose' - (function) Called when an entry is selected. Passed the
+ *                             entry value.
+ *     'onHide' - (function) Called when the list is hidden. Passed the
+ *                           list container element.
+ *     'onShow' - (function) Called when the list is shown. Passed the
+ *                           list container element.
+ *     'domParent' - (Element) Specifies the parent element. Defaults to
+ *                              document.body
+ *     'keydownObserver - (Element) The element to register the keydown handler
+ *                                  on. Defaults to document.
+ * });
+ *
+ * [base = (Element) The element to use for display positioning purposes]
+ *
+ * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+var KeyNavList = Class.create({
+
+    // Vars used: active, div, iefix, ignore, onClickFunc, onKeyDownFunc,
+    //            resizeFunc, selected
+
+    initialize: function(base, opts)
+    {
+        this.active = false;
+        this.base = $(base);
+
+        this.opts = Object.extend({
+            onChoose: Prototype.emptyFunction,
+            onHide: Prototype.emptyFunction,
+            onShow: Prototype.emptyFunction,
+            domParent: null,
+            keydownObserver: document
+        }, opts || {});
+
+        this.div = new Element('DIV', { className: 'KeyNavList' }).hide().insert(new Element('UL'));
+
+        if (!this.opts.domParent) {
+            this.opts.domParent = document.body;
+        }
+        this.opts.domParent = $(this.opts.domParent);
+
+        this.opts.domParent.insert(this.div);
+
+        if (this.opts.list) {
+            this.update(this.opts.list);
+            delete this.opts.list;
+        }
+
+        this.onClickFunc = this.onClick.bindAsEventListener(this);
+        document.observe('click', this.onClickFunc);
+
+        this.onKeyDownFunc = this.onKeyDown.bindAsEventListener(this);
+        $(this.opts.keydownObserver).observe('keydown', this.onKeyDownFunc);
+
+        this.resizeFunc = this.hide.bind(this);
+        Event.observe(window, 'resize', this.resizeFunc);
+
+        if (Prototype.Browser.IE && !window.XMLHttpRequest) {
+            this.iefix = $('knl_iframe_iefix');
+            if (!this.iefix) {
+                this.iefix = new Element('IFRAME', { id: 'knl_iframe_iefix', src: 'javascript:false;', scrolling: 'no', frameborder: 0 }).setStyle({ position: 'absolute', display: 'block', zIndex: 99 }).hide();
+                document.body.appendChild(this.iefix);
+            }
+            this.div.setStyle({ zIndex: 100 });
+        }
+    },
+
+    destroy: function()
+    {
+        document.stopObserving('click', this.onClickFunc);
+        document.stopObserving('keydown', this.onKeyDownFunc);
+        Event.stopObserving(window, 'resize', this.resizeFunc);
+        this.div.remove();
+        if (this.iefix) {
+            this.iefix.remove();
+        }
+    },
+
+    update: function(list)
+    {
+        var df = document.createDocumentFragment();
+
+        list.each(function(v) {
+            var li;
+            if (this.opts.esc) {
+                v.l = v.l.escapeHTML().gsub('  ', ' &nbsp;');
+            }
+            li = new Element('LI').insert(v.l).store('v', v.v);
+            if (v.s) {
+                this.markSelected(li);
+            }
+            df.appendChild(li);
+        }.bind(this));
+
+        this.div.down().childElements().invoke('remove');
+        this.div.down().appendChild(df);
+    },
+
+    updateBase: function(base)
+    {
+        this.base = $(base);
+    },
+
+    show: function(list)
+    {
+        this.active = true;
+
+        if (!Object.isUndefined(list)) {
+            this.update(list);
+        } else if (this.div.visible()) {
+            return;
+        }
+
+        this.opts.onShow(this.div);
+
+        this.div.setStyle({ height: null, width: null, top: null }).clonePosition(this.base, {
+            setHeight: false,
+            setWidth: false,
+            offsetTop: this.base.getHeight()
+        });
+
+        if (this.div.visible()) {
+            this._sizeDiv();
+        } else {
+            this.div.appear({
+                afterFinish: function() {
+                    if (this.selected) {
+                        this.div.scrollTop = this.selected.offsetTop;
+                    }
+                }.bind(this),
+                afterSetup: this._sizeDiv.bind(this),
+                duration: 0.15
+            });
+        }
+    },
+
+    _sizeDiv: function()
+    {
+        var divL = this.div.getLayout(),
+            dl = divL.get('left'),
+            dt = divL.get('top'),
+            off = this.opts.domParent.cumulativeOffset(),
+            v = document.viewport.getDimensions();
+
+        if ((divL.get('border-box-height') + dt + off.top + 10) > v.height) {
+            this.div.setStyle({
+                height: (v.height - dt - off.top - 10) + 'px',
+                width: (this.div.scrollWidth + 5) + 'px'
+            });
+        }
+
+        /* Need to do width second - horizontal scrolling might add scroll
+         * bar. */
+        if ((divL.get('border-box-width') + dl + off.left + 5) > v.width) {
+            dl = (v.width - divL.get('border-box-width') - off.left - 5);
+            this.div.setStyle({ left: dl + 'px' });
+        }
+
+        if (this.iefix) {
+            this.iefix.clonePosition(this.div);
+        }
+    },
+
+    hide: function()
+    {
+        if (this.div.visible()) {
+            this.active = false;
+            this.opts.onHide(this.div);
+            this.div.fade({ duration: 0.15 });
+            if (this.iefix) {
+                this.iefix.hide();
+            }
+        }
+    },
+
+    onKeyDown: function(e)
+    {
+        if (!this.active) {
+            return;
+        }
+
+        switch (e.keyCode) {
+        case Event.KEY_TAB:
+        case Event.KEY_RETURN:
+            this.opts.onChoose(this.getCurrentEntry());
+            this.hide();
+            e.stop();
+            return;
+
+        case Event.KEY_ESC:
+            this.hide();
+            e.stop();
+            return;
+
+        case Event.KEY_UP:
+            this.markPrevious();
+            e.stop();
+            return;
+
+        case Event.KEY_DOWN:
+            this.markNext();
+            e.stop();
+            return;
+        }
+    },
+
+    onClick: function(e)
+    {
+        if (this.active && this.ignore != e) {
+            var elt = e.findElement('LI');
+
+            if (elt &&
+                (elt == this.div || elt.descendantOf(this.div))) {
+                this.markSelected(elt);
+                this.opts.onChoose(this.getCurrentEntry());
+                e.stop();
+            }
+            this.hide();
+        }
+
+        this.ignore = null;
+    },
+
+    ignoreClick: function(e)
+    {
+        this.ignore = e;
+    },
+
+    setSelected: function(value)
+    {
+        this.markSelected(this.div.down().childElements().find(function(e) {
+            return e.retrieve('v') == value;
+        }));
+    },
+
+    markSelected: function(elt)
+    {
+        if (this.selected) {
+            this.selected.removeClassName('selected');
+        }
+        this.selected = elt
+            ? elt.addClassName('selected')
+            : null;
+    },
+
+    markPrevious: function()
+    {
+        this.markSelected(this.selected ? this.selected.previous() : null);
+    },
+
+    markNext: function()
+    {
+        var elt = this.selected
+            ? this.selected.next()
+            : this.div.down().childElements().first();
+
+        if (elt) {
+            this.markSelected(elt);
+        }
+    },
+
+    getCurrentEntry: function()
+    {
+        return this.selected
+            ? this.selected.retrieve('v')
+            : null;
+    }
+
+});
diff --git a/horde/js/quickfinder.js b/horde/js/quickfinder.js
new file mode 100644 (file)
index 0000000..0233252
--- /dev/null
@@ -0,0 +1,99 @@
+/**
+ * Component for filtering a table or any list of children based on
+ * the dynamic value of a text input. It requires the prototype.js
+ * library.
+ *
+ * You should define the CSS class .QuickFinderNoMatch to say what
+ * happens to items that don't match the criteria. A reasonable
+ * default would be display:none.
+ *
+ * This code is heavily inspired by Filterlicious by Gavin
+ * Kistner. The filterlicious JavaScript file did not have a license;
+ * however, most of Gavin's code is under the license defined by
+ * http://phrogz.net/JS/_ReuseLicense.txt, so I'm including that URL
+ * and Gavin's name as acknowledgements.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+var QuickFinder = {
+
+    attachBehavior: function(input) {
+        var filterTarget = input.readAttribute('for');
+        if (!filterTarget) {
+            return;
+        }
+
+        if (filterTarget.indexOf(',') != -1) {
+            input.filterTargets = [];
+            var targets = filterTarget.split(',');
+            for (var i = 0; i < targets.length; ++i) {
+                var t = $(targets[i]);
+                if (t) {
+                    input.filterTargets.push(t);
+                }
+            }
+            if (!input.filterTargets.size()) {
+                return;
+            }
+        } else {
+            input.filterTargets = [ $(filterTarget) ];
+            if (!input.filterTargets[0]) {
+                return;
+            }
+        }
+
+        var filterEmpty = input.readAttribute('empty');
+        if (filterEmpty) {
+            input.filterEmpty = $(filterEmpty);
+        }
+
+        input.observe('keyup', this.onKeyUp.bindAsEventListener(this));
+
+        for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) {
+            input.filterTargets[i].childElements().each(function(line) {
+                var filterText = line.filterText || line.readAttribute('filterText');
+                if (!filterText) {
+                    line.filterText = line.innerHTML.stripTags();
+                }
+                line.filterText = line.filterText.toLowerCase();
+            });
+        }
+
+        this.filter(input);
+    },
+
+    onKeyUp: function(e) {
+        var input = e.element();
+        if (input.filterTargets) {
+            this.filter(input);
+        }
+    },
+
+    filter: function(input) {
+        var matched = 0,
+            val = input.value.toLowerCase();
+        for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) {
+            input.filterTargets[i].childElements().each(function(line) {
+                var filterText = line.filterText;
+                if (filterText.indexOf(val) == -1) {
+                    line.addClassName('QuickFinderNoMatch');
+                } else {
+                    ++matched;
+                    line.removeClassName('QuickFinderNoMatch');
+                }
+            });
+        }
+
+        try {
+            if (input.filterEmpty) {
+                (matched == 0) ? input.filterEmpty.show() : input.filterEmpty.hide();
+            }
+        } catch (e) {}
+    }
+
+}
+
+document.observe('dom:loaded', function() {
+    $$('input').each(QuickFinder.attachBehavior.bind(QuickFinder));
+});
diff --git a/horde/js/textarearesize.js b/horde/js/textarearesize.js
new file mode 100644 (file)
index 0000000..0946f7e
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * TextareaResize: a library that automatically resizes a text area based on
+ * its contents.
+ *
+ * Requires prototypejs 1.6+.
+ *
+ * Usage:
+ * ------
+ * cs = new TextareaResize(id[, options]);
+ *
+ *   id = (string|Element) DOM ID/Element object of textarea.
+ *   options = (object) Additional options:
+ *      'max_rows' - (Number) The maximum number of rows to display.
+ *      'observe_time' - (Number) The interval between form field checks.
+ *
+ * Custom Events:
+ * --------------
+ * TexareaResize:resize
+ *   Fired when the textarea is resized.
+ *   params: NONE
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ */
+
+var TextareaResize = Class.create({
+    // Variables used: elt, max_rows, size
+
+    initialize: function(id, opts)
+    {
+        opts = opts || {};
+
+        this.elt = $(id);
+        this.max_rows = opts.max_rows || 5;
+        this.size = -1;
+
+        new Form.Element.Observer(this.elt, opts.observe_time || 1, this.resize.bind(this));
+
+        this.resize();
+    },
+
+    resize: function()
+    {
+        var old_rows, rows,
+            size = $F(this.elt).length;
+
+        if (size == this.size) {
+            return;
+        }
+
+        old_rows = rows = Number(this.elt.readAttribute('rows', 1));
+
+        if (size > this.size) {
+            while (rows < this.max_rows) {
+                if (this.elt.scrollHeight == this.elt.clientHeight) {
+                    break;
+                }
+                this.elt.writeAttribute('rows', ++rows);
+            }
+        } else if (rows > 1) {
+            do {
+                this.elt.writeAttribute('rows', --rows);
+                if (this.elt.scrollHeight != this.elt.clientHeight) {
+                    this.elt.writeAttribute('rows', ++rows);
+                    break;
+                }
+            } while (rows > 1);
+        }
+
+        this.size = size;
+
+        if (rows != old_rows) {
+            this.elt.fire('TextareaResize:resize');
+        }
+    }
+
+});
index 093df53..7f64324 100644 (file)
@@ -211,7 +211,7 @@ $scripts = array(
     array('compose-base.js', 'imp'),
     array('compose-dimp.js', 'imp'),
     array('md5.js', 'horde'),
-    array('TextareaResize.js', 'horde')
+    array('textarearesize.js', 'horde')
 );
 
 if (!($prefs->isLocked('default_encrypt')) &&
index 6377d48..4d32807 100644 (file)
@@ -579,7 +579,7 @@ if ($browser->hasFeature('javascript')) {
                 Horde::logMessage($e, 'ERR');
             }
         }
-        Horde::addScriptFile('ieEscGuard.js', 'horde');
+        Horde::addScriptFile('ieescguard.js', 'horde');
     }
 }
 
index 8269682..3ad9fe6 100644 (file)
@@ -18,12 +18,12 @@ require_once dirname(__FILE__) . '/lib/Application.php';
 Horde_Registry::appInit('imp', array('impmode' => 'dimp'));
 
 $scripts = array(
-    array('DimpBase.js', 'imp'),
-    array('ViewPort.js', 'imp'),
+    array('dimpbase.js', 'imp'),
+    array('viewport.js', 'imp'),
     array('dialog.js', 'imp'),
     array('mailbox-dimp.js', 'imp'),
     array('imp.js', 'imp'),
-    array('ContextSensitive.js', 'horde'),
+    array('contextsensitive.js', 'horde'),
     array('dragdrop2.js', 'horde'),
     array('popup.js', 'horde'),
     array('redbox.js', 'horde'),
diff --git a/imp/js/DimpBase.js b/imp/js/DimpBase.js
deleted file mode 100644 (file)
index e640987..0000000
+++ /dev/null
@@ -1,3143 +0,0 @@
-/**
- * DimpBase.js - Javascript used in the base DIMP page.
- *
- * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-var DimpBase = {
-    // Vars used and defaulting to null/false:
-    //   cfolderaction, folder, folderswitch, pollPE, pp, preview_replace,
-    //   resize, rownum, search, splitbar, template, uid, viewport
-    // msglist_template_horiz and msglist_template_vert set via
-    //   js/mailbox-dimp.js
-    cacheids: {},
-    lastrow: -1,
-    pivotrow: -1,
-    ppcache: {},
-    ppfifo: [],
-    showunsub: 0,
-    tcache: {},
-
-    // Preview pane cache size is 20 entries. Given that a reasonable guess
-    // of an average e-mail size is 10 KB (including headers), also make
-    // an estimate that the JSON data size will be approx. 10 KB. 200 KB
-    // should be a fairly safe caching value for any recent browser.
-    ppcachesize: 20,
-
-    // Message selection functions
-
-    // id = (string) DOM ID
-    // opts = (Object) Boolean options [ctrl, right, shift]
-    msgSelect: function(id, opts)
-    {
-        var bounds,
-            row = this.viewport.createSelection('domid', id),
-            rownum = row.get('rownum').first(),
-            sel = this.isSelected('domid', id),
-            selcount = this.selectedCount();
-
-        this.lastrow = rownum;
-
-        // Some browsers need to stop the mousedown event before it propogates
-        // down to the browser level in order to prevent text selection on
-        // drag/drop actions.  Clicking on a message should always lose focus
-        // from the search input, because the user may immediately start
-        // keyboard navigation after that. Thus, we need to ensure that a
-        // message click loses focus on the search input.
-        if ($('qsearch')) {
-            $('qsearch_input').blur();
-        }
-
-        if (opts.shift) {
-            if (selcount) {
-                if (!sel || selcount != 1) {
-                    bounds = [ rownum, this.pivotrow ];
-                    this.viewport.select($A($R(bounds.min(), bounds.max())), { range: true });
-                }
-                return;
-            }
-        } else if (opts.ctrl) {
-            this.pivotrow = rownum;
-            if (sel) {
-                this.viewport.deselect(row, { right: opts.right });
-                return;
-            } else if (opts.right || selcount) {
-                this.viewport.select(row, { add: true, right: opts.right });
-                return;
-            }
-        }
-
-        this.viewport.select(row, { right: opts.right });
-    },
-
-    selectAll: function()
-    {
-        this.viewport.select(this.viewport.getAllRows(), { range: true });
-    },
-
-    isSelected: function(format, data)
-    {
-        return this.viewport.getSelected().contains(format, data);
-    },
-
-    selectedCount: function()
-    {
-        return (this.viewport) ? this.viewport.getSelected().size() : 0;
-    },
-
-    resetSelected: function()
-    {
-        if (this.viewport) {
-            this.viewport.deselect(this.viewport.getSelected(), { clearall: true });
-        }
-        this.toggleButtons();
-        this.clearPreviewPane();
-    },
-
-    // num = (integer) See absolute.
-    // absolute = Is num an absolute row number - from 1 -> page_size (true) -
-    //            or a relative change from the current selected value (false)
-    //            If no current selected value, the first message in the
-    //            current viewport is selected.
-    moveSelected: function(num, absolute)
-    {
-        var curr, curr_row, row, row_data, sel;
-
-        if (absolute) {
-            if (!this.viewport.getMetaData('total_rows')) {
-                return;
-            }
-            curr = num;
-        } else {
-            if (num == 0) {
-                return;
-            }
-
-            sel = this.viewport.getSelected();
-            switch (sel.size()) {
-            case 0:
-                curr = this.viewport.currentOffset();
-                curr += (num > 0) ? 1 : this.viewport.getPageSize('current');
-                break;
-
-            case 1:
-                curr_row = sel.get('dataob').first();
-                curr = curr_row.VP_rownum + num;
-                break;
-
-            default:
-                sel = sel.get('rownum');
-                curr = (num > 0 ? sel.max() : sel.min()) + num;
-                break;
-            }
-            curr = (num > 0) ? Math.min(curr, this.viewport.getMetaData('total_rows')) : Math.max(curr, 1);
-        }
-
-        row = this.viewport.createSelection('rownum', curr);
-        if (row.size()) {
-            row_data = row.get('dataob').first();
-            if (!curr_row || row_data.imapuid != curr_row.imapuid) {
-                this.viewport.scrollTo(row_data.VP_rownum);
-                this.viewport.select(row, { delay: 0.3 });
-            }
-        } else {
-            this.rownum = curr;
-            this.viewport.requestContentRefresh(curr - 1);
-        }
-    },
-    // End message selection functions
-
-    go: function(loc, data)
-    {
-        var app, f, separator;
-
-        /* If switching from options, we need to reload page to pick up any
-         * prefs changes. */
-        if (this.folder === null &&
-            loc != 'options' &&
-            $('appoptions') &&
-            $('appoptions').hasClassName('on')) {
-            $('dimpPage').hide();
-            $('dimpLoading').show();
-            return DimpCore.redirect(DIMP.conf.URI_DIMP + '#' + loc, true);
-        }
-
-        if (loc.startsWith('compose:')) {
-            return;
-        }
-
-        if (loc.startsWith('msg:')) {
-            separator = loc.indexOf(':', 4);
-            f = loc.substring(4, separator);
-            this.uid = parseInt(loc.substring(separator + 1), 10);
-            loc = 'folder:' + f;
-            // Now fall through to the 'folder:' check below.
-        }
-
-        if (loc.startsWith('folder:')) {
-            f = loc.substring(7);
-            if (this.folder != f || !$('dimpmain_folder').visible()) {
-                this.highlightSidebar(this.getFolderId(f));
-                if (!$('dimpmain_folder').visible()) {
-                    $('dimpmain_portal').hide();
-                    $('dimpmain_folder').show();
-                }
-
-                // This catches the refresh case - no need to re-add to history
-                if (!Object.isUndefined(this.folder) && !this.search) {
-                    location.hash = encodeURIComponent(loc);
-                }
-            }
-
-            this.loadMailbox(f);
-            return;
-        }
-
-        f = this.folder;
-        this.folder = null;
-        $('dimpmain_folder').hide();
-        $('dimpmain_portal').update(DIMP.text.loading).show();
-
-        if (loc.startsWith('app:')) {
-            app = loc.substr(4);
-            if (app == 'imp') {
-                this.go('folder:INBOX');
-                return;
-            }
-            this.highlightSidebar('app' + app);
-            location.hash = encodeURIComponent(loc);
-            if (data) {
-                this.iframeContent(loc, data);
-            } else if (DIMP.conf.app_urls[app]) {
-                this.iframeContent(loc, DIMP.conf.app_urls[app]);
-            }
-            return;
-        }
-
-        switch (loc) {
-        case 'search':
-            // data: 'edit_query' = folder to edit; otherwise, loads search
-            //       screen with current mailbox as default search mailbox
-            if (!data) {
-                data = { search_mailbox: f };
-            }
-            this.highlightSidebar();
-            DimpCore.setTitle(DIMP.text.search);
-            this.iframeContent(loc, DimpCore.addURLParam(DIMP.conf.URI_SEARCH, data));
-            break;
-
-        case 'portal':
-            this.highlightSidebar('appportal');
-            location.hash = encodeURIComponent(loc);
-            DimpCore.setTitle(DIMP.text.portal);
-            DimpCore.doAction('showPortal', {}, { callback: this._portalCallback.bind(this) });
-            break;
-
-        case 'options':
-            this.highlightSidebar('appoptions');
-            location.hash = encodeURIComponent(loc);
-            DimpCore.setTitle(DIMP.text.prefs);
-            this.iframeContent(loc, DIMP.conf.URI_PREFS_IMP);
-            break;
-        }
-    },
-
-    highlightSidebar: function(id)
-    {
-        // Folder bar may not be fully loaded yet.
-        if ($('foldersLoading').visible()) {
-            this.highlightSidebar.bind(this, id).defer();
-            return;
-        }
-
-        var curr = $('sidebar').down('.on'),
-            elt = $(id);
-
-        if (curr == elt) {
-            return;
-        }
-
-        if (elt && !elt.match('LI')) {
-            elt = elt.up();
-            if (!elt) {
-                return;
-            }
-        }
-
-        if (curr) {
-            curr.removeClassName('on');
-        }
-
-        if (elt) {
-            elt.addClassName('on');
-            this._toggleSubFolder(elt, 'exp');
-        }
-    },
-
-    iframeContent: function(name, loc)
-    {
-        var container = $('dimpmain_portal'), iframe;
-        if (!container) {
-            DimpCore.showNotifications([ { type: 'horde.error', message: 'Bad portal!' } ]);
-            return;
-        }
-
-        iframe = new Element('IFRAME', { id: 'iframe' + (name === null ? loc : name), className: 'iframe', frameBorder: 0, src: loc }).setStyle({ height: document.viewport.getHeight() + 'px' });
-        container.insert(iframe);
-    },
-
-    // r = ViewPort row data
-    msgWindow: function(r)
-    {
-        this.updateSeenUID(r, 1);
-        var url = DIMP.conf.URI_MESSAGE;
-        url += (url.include('?') ? '&' : '?') +
-               $H({ folder: r.view,
-                    uid: Number(r.imapuid) }).toQueryString();
-        DimpCore.popupWindow(url, 'msgview' + r.view + r.imapuid);
-    },
-
-    composeMailbox: function(type)
-    {
-        var sel = this.viewport.getSelected();
-        if (!sel.size()) {
-            return;
-        }
-        sel.get('dataob').each(function(s) {
-            DimpCore.compose(type, { folder: s.view, uid: s.imapuid });
-        });
-    },
-
-    loadMailbox: function(f, opts)
-    {
-        var need_delete;
-        opts = opts || {};
-
-        if (!this.viewport) {
-            this._createViewPort();
-        }
-
-        if (!opts.background) {
-            this.resetSelected();
-            this.quicksearchClear(true);
-
-            if (this.folder != f) {
-                $('folderName').update(DIMP.text.loading);
-                $('msgHeader').update();
-                this.folderswitch = true;
-
-                /* Don't cache results of search folders - since we will need
-                 * to grab new copy if we ever return to it. */
-                if (this.isSearch(this.folder)) {
-                    need_delete = this.folder;
-                }
-
-                this.folder = f;
-
-                if (this.isSearch(f)) {
-                    if (!this.search || this.search.flag) {
-                        this._quicksearchDeactivate(!this.search);
-                    }
-                    $('refreshlink').show();
-                } else {
-                    $('refreshlink').hide();
-                }
-            }
-        }
-
-        this.viewport.loadView(f, { search: (this.uid ? { imapuid: Number(this.uid) } : null), background: opts.background});
-
-        if (need_delete) {
-            this.viewport.deleteView(need_delete);
-        }
-    },
-
-    _createViewPort: function()
-    {
-        var container = $('msgSplitPane');
-
-        [ $('msglistHeader') ].invoke(DIMP.conf.preview_pref == 'vert' ? 'hide' : 'show');
-
-        this.template = {
-            horiz: new Template(this.msglist_template_horiz),
-            vert: new Template(this.msglist_template_vert)
-        };
-
-        this.viewport = new ViewPort({
-            // Mandatory config
-            ajax_url: DIMP.conf.URI_AJAX + 'viewPort',
-            container: container,
-            onContent: function(r, mode) {
-                var bg, re, u,
-                    thread = $H(this.viewport.getMetaData('thread')),
-                    tsort = (this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v);
-
-                r.subjectdata = r.status = '';
-                r.subjecttitle = r.subject;
-
-                // Add thread graphics
-                if (tsort) {
-                    u = thread.get(r.imapuid);
-                    if (u) {
-                        $R(0, u.length, true).each(function(i) {
-                            var c = u.charAt(i);
-                            if (!this.tcache[c]) {
-                                this.tcache[c] = '<span class="treeImg treeImg' + c + '"></span>';
-                            }
-                            r.subjectdata += this.tcache[c];
-                        }, this);
-                    }
-                }
-
-                /* Generate the status flags. */
-                if (r.flag) {
-                    r.flag.each(function(a) {
-                        var ptr = DIMP.conf.flags[a];
-                        if (ptr.p) {
-                            if (!ptr.elt) {
-                                /* Until text-overflow is supported on all
-                                 * browsers, need to truncate label text
-                                 * ourselves. */
-                                ptr.elt = '<span class="' + ptr.c + '" title="' + ptr.l + '" style="background:' + ptr.b + ';color:' + ptr.f + '">' + ptr.l.truncate(10) + '</span>';
-                            }
-                            r.subjectdata += ptr.elt;
-                        } else {
-                            if (!ptr.elt) {
-                                ptr.elt = '<div class="msgflags ' + ptr.c + '" title="' + ptr.l + '"></div>';
-                            }
-                            r.status += ptr.elt;
-
-                            r.VP_bg.push(ptr.c);
-
-                            if (ptr.b) {
-                                bg = ptr.b;
-                            }
-                        }
-                    });
-                }
-
-                // Set bg
-                if (bg) {
-                    r.style = 'background:' + bg;
-                }
-
-                // Check for search strings
-                if (this.isSearch(null, true)) {
-                    re = new RegExp("(" + $F('qsearch_input') + ")", "i");
-                    [ 'from', 'subject' ].each(function(h) {
-                        r[h] = r[h].gsub(re, '<span class="qsearchMatch">#{1}</span>');
-                    });
-                }
-
-                // If these fields are null, invalid string was scrubbed by
-                // JSON encode.
-                if (r.from === null) {
-                    r.from = '[' + DIMP.text.badaddr + ']';
-                }
-                if (r.subject === null) {
-                    r.subject = r.subjecttitle = '[' + DIMP.text.badsubject + ']';
-                }
-
-                r.VP_bg.push('vpRow');
-
-                switch (mode) {
-                case 'vert':
-                    r.VP_bg.unshift('vpRowVert');
-                    r.className = r.VP_bg.join(' ');
-                    return this.template.vert.evaluate(r);
-
-                default:
-                    r.VP_bg.unshift('vpRowHoriz');
-                    r.className = r.VP_bg.join(' ');
-                    return this.template.horiz.evaluate(r);
-                }
-            }.bind(this),
-
-            // Optional config
-            ajax_opts: Object.clone(DimpCore.doActionOpts),
-            buffer_pages: DIMP.conf.buffer_pages,
-            empty_msg: DIMP.text.vp_empty,
-            list_class: 'msglist',
-            page_size: DIMP.conf.splitbar_pos,
-            pane_data: 'previewPane',
-            pane_mode: DIMP.conf.preview_pref,
-            split_bar_class: { horiz: 'splitBarHoriz', vert: 'splitBarVert' },
-            wait: DIMP.conf.viewport_wait,
-
-            // Callbacks
-            onAjaxFailure: function() {
-                if ($('dimpmain_folder').visible()) {
-                    DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.listmsg_timeout } ]);
-                }
-                this.loadingImg('viewport', false);
-            }.bind(this),
-            onAjaxRequest: function(id) {
-                var p = $H();
-                if (this.folderswitch && this.isSearch(id, true)) {
-                    p.set('qsearchmbox', this.search.mbox);
-                    if (this.search.flag) {
-                        p.update({ qsearchflag: this.search.flag, qsearchflagnot: Number(this.convertFlag(this.search.flag, this.search.not)) });
-                    } else {
-                        p.set('qsearch', $F('qsearch_input'));
-                    }
-                }
-                return DimpCore.addRequestParams(p);
-            }.bind(this),
-            onAjaxResponse: function(o, h) {
-                DimpCore.doActionComplete(o);
-            },
-            onCachedList: function(id) {
-                if (!this.cacheids[id]) {
-                    var vs = this.viewport.getSelection(id);
-                    if (!vs.size()) {
-                        return '';
-                    }
-
-                    this.cacheids[id] = DimpCore.toRangeString(DimpCore.selectionToRange(vs));
-                }
-                return this.cacheids[id];
-            }.bind(this),
-            onContentOffset: function(offset) {
-                if (this.uid) {
-                    var row = this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ this.uid ] }, view: { equal: [ this.folder ] } });
-                    if (row.size()) {
-                        this.rownum = row.get('rownum').first();
-                    }
-                    this.uid = null;
-                }
-
-                if (this.rownum) {
-                    this.viewport.scrollTo(this.rownum, { noupdate: true, top: true });
-                    offset = this.viewport.currentOffset();
-                }
-
-                return offset;
-            }.bind(this),
-            onSlide: this.setMessageListTitle.bind(this)
-        });
-
-        /* Custom ViewPort events. */
-        container.observe('ViewPort:add', function(e) {
-            var row = e.memo.identify();
-            DimpCore.addContextMenu({
-                id: row,
-                type: 'message'
-            });
-            new Drag(row, this._msgDragConfig);
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:cacheUpdate', function(e) {
-            delete this.cacheids[e.memo];
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:clear', function(e) {
-            this._removeMouseEvents(e.memo);
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:contentComplete', function() {
-            var flags, ssc, tmp,
-                ham = spam = 'show',
-                l = this.viewport.getMetaData('label');
-
-            this.setMessageListTitle();
-            if (!this.isSearch()) {
-                this.setFolderLabel(this.folder, this.viewport.getMetaData('unseen') || 0);
-            }
-            this.updateTitle();
-
-            if (this.rownum) {
-                this.viewport.select(this.viewport.createSelection('rownum', this.rownum));
-                this.rownum = null;
-            }
-
-            // 'label' will not be set if there has been an error
-            // retrieving data from the server.
-            l = this.viewport.getMetaData('label');
-            if (l) {
-                if (this.isSearch(null, true)) {
-                    l += ' (' + this.search.label + ')';
-                }
-                $('folderName').update(l);
-            }
-
-            if (this.folderswitch) {
-                this.folderswitch = false;
-
-                tmp = $('applyfilterlink');
-                if (tmp) {
-                    if (this.isSearch() ||
-                        (!DIMP.conf.filter_any &&
-                         this.folder.toUpperCase() != 'INBOX')) {
-                        tmp.hide();
-                    } else {
-                        tmp.show();
-                    }
-                }
-
-                if (this.folder == DIMP.conf.spam_mbox) {
-                    if (!DIMP.conf.spam_spammbox) {
-                        spam = 'hide';
-                    }
-                } else if (DIMP.conf.ham_spammbox) {
-                    ham = 'hide';
-                }
-
-                if ($('button_ham')) {
-                    [ $('button_ham').up(), $('ctx_message_ham') ].invoke(ham);
-                }
-                if ($('button_spam')) {
-                    [ $('button_spam').up(), $('ctx_message_spam') ].invoke(spam);
-                }
-
-                /* Read-only changes. 'oa_setflag' is handled elsewhere. */
-                tmp = [ $('button_deleted') ].compact().invoke('up', 'SPAN').concat($('ctx_message_deleted', 'ctx_message_setflag', 'ctx_message_undeleted'));
-
-                if (this.viewport.getMetaData('readonly')) {
-                    tmp.compact().invoke('hide');
-                    $('folderName').next().show();
-                } else {
-                    tmp.compact().invoke('show');
-                    $('folderName').next().hide();
-                }
-            } else if (this.filtertoggle &&
-                       this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v) {
-                ssc = $H(DIMP.conf.sort).get('date').v;
-            }
-
-            this.setSortColumns(ssc);
-
-            /* Context menu: generate the list of settable flags for this
-             * mailbox. */
-            flags = this.viewport.getMetaData('flags');
-            $('ctx_message_setflag', 'oa_setflag').invoke('up').invoke(flags.size() ? 'show' : 'hide');
-            if (flags.size()) {
-                $('ctx_flag').childElements().each(function(c) {
-                    [ c ].invoke(flags.include(c.readAttribute('flag')) ? 'show' : 'hide');
-                });
-            }
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:deselect', function(e) {
-            var sel = this.viewport.getSelected(),
-                count = sel.size();
-            if (!count) {
-                this.lastrow = this.pivotrow = -1;
-            }
-
-            this.toggleButtons();
-            if (e.memo.opts.right || !count) {
-                if (!this.preview_replace) {
-                    this.clearPreviewPane();
-                }
-            } else if ((count == 1) && DIMP.conf.preview_pref) {
-                this.loadPreview(sel.get('dataob').first());
-            }
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:endFetch', this.loadingImg.bind(this, 'viewport', false));
-
-        container.observe('ViewPort:fetch', this.loadingImg.bind(this, 'viewport', true));
-
-        container.observe('ViewPort:select', function(e) {
-            var d = e.memo.vs.get('rownum');
-            if (d.size() == 1) {
-                this.lastrow = this.pivotrow = d.first();
-            }
-
-            this.toggleButtons();
-
-            if (DIMP.conf.preview_pref) {
-                if (e.memo.opts.right) {
-                    this.clearPreviewPane();
-                } else {
-                    if (e.memo.opts.delay) {
-                        this.initPreviewPane.bind(this).delay(e.memo.opts.delay);
-                    } else {
-                        this.initPreviewPane();
-                    }
-                }
-            }
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:splitBarChange', function(e) {
-            if (e.memo = 'horiz') {
-                this._updatePrefs('dimp_splitbar', this.viewport.getPageSize());
-            }
-        }.bindAsEventListener(this));
-
-        container.observe('ViewPort:wait', function() {
-            if ($('dimpmain_folder').visible()) {
-                DimpCore.showNotifications([ { type: 'horde.warning', message: DIMP.text.listmsg_wait } ]);
-            }
-        });
-    },
-
-    _removeMouseEvents: function(elt)
-    {
-        var d, id = $(elt).readAttribute('id');
-
-        if (id) {
-            if (d = DragDrop.Drags.getDrag(id)) {
-                d.destroy();
-            }
-
-            DimpCore.DMenu.removeElement(id);
-        }
-    },
-
-    contextOnClick: function(parentfunc, e)
-    {
-        var flag, tmp,
-            baseelt = e.element(),
-            elt = e.memo.elt,
-            id = elt.readAttribute('id'),
-            menu = e.memo.trigger;
-
-        switch (id) {
-        case 'ctx_folder_create':
-            this.createSubFolder(baseelt);
-            break;
-
-        case 'ctx_container_rename':
-        case 'ctx_folder_rename':
-            this.renameFolder(baseelt);
-            break;
-
-        case 'ctx_folder_empty':
-            tmp = baseelt.up('LI');
-            if (window.confirm(DIMP.text.empty_folder.sub('%s', tmp.readAttribute('title')))) {
-                DimpCore.doAction('emptyMailbox', { mbox: tmp.retrieve('mbox') }, { callback: this._emptyMailboxCallback.bind(this) });
-            }
-            break;
-
-        case 'ctx_folder_delete':
-        case 'ctx_vfolder_delete':
-            tmp = baseelt.up('LI');
-            if (window.confirm(DIMP.text.delete_folder.sub('%s', tmp.readAttribute('title')))) {
-                DimpCore.doAction('deleteMailbox', { mbox: tmp.retrieve('mbox') }, { callback: this.mailboxCallback.bind(this) });
-            }
-            break;
-
-        case 'ctx_folder_seen':
-        case 'ctx_folder_unseen':
-            this.flagAll('\\seen', id == 'ctx_folder_seen', baseelt.up('LI').retrieve('mbox'));
-            break;
-
-        case 'ctx_folder_poll':
-        case 'ctx_folder_nopoll':
-            this.modifyPoll(baseelt.up('LI').retrieve('mbox'), id == 'ctx_folder_poll');
-            break;
-
-        case 'ctx_folder_sub':
-        case 'ctx_folder_unsub':
-            this.subscribeFolder(baseelt.up('LI').retrieve('mbox'), id == 'ctx_folder_sub');
-            break;
-
-        case 'ctx_container_create':
-            this.createSubFolder(baseelt);
-            break;
-
-        case 'ctx_folderopts_new':
-            this.createBaseFolder();
-            break;
-
-        case 'ctx_folderopts_sub':
-        case 'ctx_folderopts_unsub':
-            this.toggleSubscribed();
-            break;
-
-        case 'ctx_folderopts_expand':
-        case 'ctx_folderopts_collapse':
-            this._toggleSubFolder($('normalfolders'), id == 'ctx_folderopts_expand' ? 'expall' : 'colall', true);
-            break;
-
-        case 'ctx_folderopts_reload':
-            this._reloadFolders();
-            break;
-
-        case 'ctx_container_expand':
-        case 'ctx_container_collapse':
-        case 'ctx_folder_expand':
-        case 'ctx_folder_collapse':
-            this._toggleSubFolder(baseelt.up('LI').next(), (id == 'ctx_container_expand' || id == 'ctx_folder_expand') ? 'expall' : 'colall', true);
-            break;
-
-        case 'ctx_message_spam':
-        case 'ctx_message_ham':
-            this.reportSpam(id == 'ctx_message_spam');
-            break;
-
-        case 'ctx_message_blacklist':
-        case 'ctx_message_whitelist':
-            this.blacklist(id == 'ctx_message_blacklist');
-            break;
-
-        case 'ctx_message_deleted':
-            this.deleteMsg();
-            break;
-
-        case 'ctx_message_forward':
-        case 'ctx_message_reply':
-            this.composeMailbox(id == 'ctx_message_forward' ? 'forward_auto' : 'reply_auto');
-            break;
-
-        case 'ctx_message_source':
-            this.viewport.getSelected().get('dataob').each(function(v) {
-                DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: v.imapuid, mailbox: v.view, actionID: 'view_source', id: 0 }, true), v.imapuid + '|' + v.view);
-            }, this);
-            break;
-
-        case 'ctx_message_resume':
-            this.composeMailbox('resume');
-            break;
-
-        case 'ctx_reply_reply':
-        case 'ctx_reply_reply_all':
-        case 'ctx_reply_reply_list':
-            this.composeMailbox(id.substring(10));
-            break;
-
-        case 'ctx_forward_attach':
-        case 'ctx_forward_body':
-        case 'ctx_forward_both':
-        case 'ctx_forward_redirect':
-            this.composeMailbox(id.substring(4));
-            break;
-
-        case 'oa_preview_hide':
-            DIMP.conf.preview_pref_old = DIMP.conf.preview_pref;
-            this.togglePreviewPane('');
-            break;
-
-        case 'oa_preview_show':
-            this.togglePreviewPane(DIMP.conf.preview_pref_old || 'horiz');
-            break;
-
-        case 'oa_layout_horiz':
-        case 'oa_layout_vert':
-            this.togglePreviewPane(id.substring(10));
-            break;
-
-        case 'oa_blacklist':
-        case 'oa_whitelist':
-            this.blacklist(id == 'oa_blacklist');
-            break;
-
-        case 'ctx_message_undeleted':
-        case 'oa_undeleted':
-            this.flag('\\deleted', false);
-            break;
-
-        case 'oa_selectall':
-            this.selectAll();
-            break;
-
-        case 'oa_purge_deleted':
-            this.purgeDeleted();
-            break;
-
-        case 'ctx_vfolder_edit':
-            tmp = { edit_query: baseelt.up('LI').retrieve('mbox') };
-            // Fall through
-
-        case 'ctx_qsearchopts_advanced':
-            this.go('search', tmp);
-            break;
-
-        case 'ctx_qsearchby_all':
-        case 'ctx_qsearchby_body':
-        case 'ctx_qsearchby_from':
-        case 'ctx_qsearchby_to':
-        case 'ctx_qsearchby_subject':
-            DIMP.conf.qsearchfield = id.substring(14);
-            this._updatePrefs('dimp_qsearch_field', DIMP.conf.qsearchfield);
-            if (!$('qsearch').hasClassName('qsearchActive')) {
-                this._setQsearchText(true);
-            }
-            break;
-
-        case 'ctx_mboxsort_none':
-            this.sort($H(DIMP.conf.sort).get('sequence').v);
-            break;
-
-        default:
-            if (menu.endsWith('_setflag') || menu.endsWith('_unsetflag')) {
-                flag = elt.readAttribute('flag');
-                this.flag(flag, this.convertFlag(flag, menu.endsWith('_setflag')));
-            } else if (menu.endsWith('_filter') || menu.endsWith('_filternot')) {
-                this.search = {
-                    flag: elt.readAttribute('flag'),
-                    label: this.viewport.getMetaData('label'),
-                    mbox: this.folder,
-                    not: menu.endsWith('_filternot')
-                };
-                this.loadMailbox(DIMP.conf.fsearchid);
-            } else {
-                parentfunc(e);
-            }
-            break;
-        }
-    },
-
-    contextOnShow: function(parentfunc, e)
-    {
-        var elts, ob, sel, tmp,
-            baseelt = e.element(),
-            ctx_id = e.memo;
-
-        switch (ctx_id) {
-        case 'ctx_folder':
-            elts = $('ctx_folder_create', 'ctx_folder_rename', 'ctx_folder_delete');
-            baseelt = baseelt.up('LI');
-
-            if (baseelt.retrieve('mbox') == 'INBOX') {
-                elts.invoke('hide');
-                if ($('ctx_folder_sub')) {
-                    $('ctx_folder_sub', 'ctx_folder_unsub').invoke('hide');
-                }
-            } else {
-                if ($('ctx_folder_sub')) {
-                    tmp = baseelt.hasClassName('unsubFolder');
-                    [ $('ctx_folder_sub') ].invoke(tmp ? 'show' : 'hide');
-                    [ $('ctx_folder_unsub') ].invoke(tmp ? 'hide' : 'show');
-                }
-
-                if (DIMP.conf.fixed_folders &&
-                    DIMP.conf.fixed_folders.indexOf(baseelt.retrieve('mbox')) != -1) {
-                    elts.shift();
-                    elts.invoke('hide');
-                } else {
-                    elts.invoke('show');
-                }
-            }
-
-            tmp = Object.isUndefined(baseelt.retrieve('u'));
-            [ $('ctx_folder_poll') ].invoke(tmp ? 'show' : 'hide');
-            [ $('ctx_folder_nopoll') ].invoke(tmp ? 'hide' : 'show');
-
-            tmp = $(this.getSubFolderId(baseelt.readAttribute('id')));
-            [ $('ctx_folder_expand').up() ].invoke(tmp ? 'show' : 'hide');
-            break;
-
-        case 'ctx_reply':
-            sel = this.viewport.getSelected();
-            if (sel.size() == 1) {
-                ob = sel.get('dataob').first();
-            }
-            [ $('ctx_reply_reply_list') ].invoke(ob && ob.listmsg ? 'show' : 'hide');
-            break;
-
-        case 'ctx_otheractions':
-            switch (DIMP.conf.preview_pref) {
-            case 'vert':
-                $('oa_preview_hide', 'oa_layout_horiz').invoke('show');
-                $('oa_preview_show', 'oa_layout_vert').invoke('hide');
-                break;
-
-            case 'horiz':
-                $('oa_preview_hide', 'oa_layout_vert').invoke('show');
-                $('oa_preview_show', 'oa_layout_horiz').invoke('hide');
-                break;
-
-            default:
-                $('oa_preview_hide', 'oa_layout_horiz', 'oa_layout_vert').invoke('hide');
-                $('oa_preview_show').show();
-                break;
-            }
-            tmp = [ $('oa_undeleted') ];
-            $('oa_blacklist', 'oa_whitelist').each(function(o) {
-                if (o) {
-                    tmp.push(o.up());
-                }
-            });
-            if ($('oa_setflag')) {
-                if (this.viewport.getMetaData('readonly')) {
-                    $('oa_setflag').up().hide();
-                } else {
-                    tmp.push($('oa_setflag').up());
-                }
-            }
-            tmp.compact().invoke(this.viewport.getSelected().size() ? 'show' : 'hide');
-            break;
-
-        case 'ctx_qsearchby':
-            $(ctx_id).descendants().invoke('removeClassName', 'contextSelected');
-            $(ctx_id + '_' + DIMP.conf.qsearchfield).addClassName('contextSelected');
-            break;
-
-        case 'ctx_message':
-            [ $('ctx_message_source').up() ].invoke(DIMP.conf.preview_pref ? 'hide' : 'show');
-            sel = this.viewport.getSelected();
-            [ $('ctx_message_resume') ].invoke(sel.size() == 1 && sel.get('dataob').first().draft ? 'show' : 'hide');
-            break;
-
-        default:
-            parentfunc(e);
-            break;
-        }
-    },
-
-    updateTitle: function()
-    {
-        var elt, unseen,
-            label = this.viewport.getMetaData('label');
-
-        if (this.isSearch(null, true)) {
-            label += ' (' + this.search.label + ')';
-        } else {
-            elt = $(this.getFolderId(this.folder));
-            if (elt) {
-                unseen = elt.retrieve('u');
-                if (unseen > 0) {
-                    label += ' (' + unseen + ')';
-                }
-            } else {
-                this.updateTitle.bind(this).defer();
-            }
-        }
-        DimpCore.setTitle(label);
-    },
-
-    sort: function(sortby)
-    {
-        var s;
-
-        if (Object.isUndefined(sortby)) {
-            return;
-        }
-
-        sortby = Number(sortby);
-        if (sortby == this.viewport.getMetaData('sortby')) {
-            s = { sortdir: (this.viewport.getMetaData('sortdir') ? 0 : 1) };
-            this.viewport.setMetaData({ sortdir: s.sortdir });
-        } else {
-            s = { sortby: sortby };
-            this.viewport.setMetaData({ sortby: s.sortby });
-        }
-
-        this.setSortColumns(sortby);
-        this.viewport.reload(s);
-    },
-
-    setSortColumns: function(sortby)
-    {
-        var hdr, tmp,
-            ptr = DIMP.conf.sort,
-            m = $('msglistHeader');
-
-        if (Object.isUndefined(sortby)) {
-            sortby = this.viewport.getMetaData('sortby');
-        }
-
-        /* Init once per load. */
-        if (Object.isHash(ptr)) {
-            m.childElements().invoke('removeClassName', 'sortup').invoke('removeClassName', 'sortdown');
-        } else {
-            DIMP.conf.sort = ptr = $H(ptr);
-            ptr.each(function(s) {
-                s.value.e = new Element('A', { className: 'widget' }).store('sortby', s.value.v).insert(s.value.t);
-            }, this);
-
-            m.down('.msgFrom').update(ptr.get('from').e).insert(ptr.get('to').e);
-            m.down('.msgSize').update(ptr.get('size').e);
-            m.down('.msgDate').update(ptr.get('date').e);
-        }
-
-        /* Toggle between From/To header. */
-        tmp = m.down('.msgFrom a');
-        if (this.viewport.getMetaData('special')) {
-            tmp.hide().next().show();
-        } else {
-            tmp.show().next().hide();
-        }
-
-        /* Toggle between Subject/Thread header. */
-        tmp = m.down('.msgSubject');
-        if (this.isSearch() ||
-            this.viewport.getMetaData('nothread')) {
-            hdr = { l: 'subject', t: tmp };
-        } else if (sortby == ptr.get('thread').v) {
-            hdr = { l: 'thread', s: 'subject', t: tmp };
-        } else {
-            hdr = { l: 'subject', s: 'thread', t: tmp };
-        }
-
-        hdr.t.update().update(ptr.get(hdr.l).e.removeClassName('smallSort').update(ptr.get(hdr.l).t));
-        if (hdr.s) {
-            hdr.t.insert(ptr.get(hdr.s).e.addClassName('smallSort').update('[' + ptr.get(hdr.s).t + ']'));
-        }
-
-        ptr.find(function(s) {
-            if (sortby != s.value.v) {
-                return false;
-            }
-            var elt = s.value.e.up();
-            if (elt) {
-                elt.addClassName(this.viewport.getMetaData('sortdir') ? 'sortup' : 'sortdown');
-            }
-            return true;
-        }, this);
-    },
-
-    // Preview pane functions
-    // mode = (string) Either 'horiz', 'vert', or empty
-    togglePreviewPane: function(mode)
-    {
-        var old = DIMP.conf.preview_pref;
-        if (mode != DIMP.conf.preview_pref) {
-            DIMP.conf.preview_pref = mode;
-            this._updatePrefs('dimp_show_preview', mode);
-            [ $('msglistHeader') ].invoke(mode == 'vert' ? 'hide' : 'show');
-            this.viewport.showSplitPane(mode);
-            if (!old) {
-                this.initPreviewPane();
-            }
-        }
-    },
-
-    loadPreview: function(data, params)
-    {
-        var pp_uid;
-
-        if (!DIMP.conf.preview_pref) {
-            return;
-        }
-
-        if (!params) {
-            if (this.pp &&
-                this.pp.imapuid == data.imapuid &&
-                this.pp.view == data.view) {
-                return;
-            }
-            this.pp = data;
-            pp_uid = this._getPPId(data.imapuid, data.view);
-
-            if (this.ppfifo.indexOf(pp_uid) != -1) {
-                  // There is a chance that the message may have been marked
-                  // as unseen since first being viewed. If so, we need to
-                  // explicitly flag as seen here. TODO?
-                if (!this.hasFlag('\\seen', data)) {
-                    this.flag('\\seen', true);
-                }
-                return this._loadPreviewCallback(this.ppcache[pp_uid]);
-            }
-        }
-
-        this.loadingImg('msg', true);
-
-        DimpCore.doAction('showPreview', this.viewport.addRequestParams(params || {}), { uids: this.viewport.createSelection('dataob', this.pp), callback: this._loadPreviewCallback.bind(this) });
-    },
-
-    _loadPreviewCallback: function(resp)
-    {
-        var bg, ppuid, row, search, tmp,
-            pm = $('previewMsg'),
-            r = resp.response.preview,
-            t = $('msgHeadersContent').down('THEAD');
-
-        bg = (this.pp &&
-              (this.pp.imapuid != r.uid || this.pp.view != r.mailbox));
-
-        if (!r.error) {
-            search = this.viewport.getSelection().search({ imapuid: { equal: [ r.uid ] }, view: { equal: [ r.mailbox ] } });
-            if (search.size()) {
-                row = search.get('dataob').first();
-                this.updateSeenUID(row, 1);
-            }
-        }
-
-        if (r.error || this.viewport.getSelected().size() != 1) {
-            if (!bg) {
-                if (r.error) {
-                    DimpCore.showNotifications([ { type: r.errortype, message: r.error } ]);
-                }
-                this.clearPreviewPane();
-            }
-            return;
-        }
-
-        // Store in cache.
-        ppuid = this._getPPId(r.uid, r.mailbox);
-        this._expirePPCache([ ppuid ]);
-        this.ppcache[ppuid] = resp;
-        this.ppfifo.push(ppuid);
-
-        if (bg) {
-            return;
-        }
-
-        DimpCore.removeAddressLinks(pm);
-
-        // Add subject
-        tmp = pm.select('.subject');
-        tmp.invoke('update', r.subject === null ? '[' + DIMP.text.badsubject + ']' : r.subject);
-
-        // Add date
-        [ $('msgHeadersColl').select('.date'), $('msgHeaderDate').select('.date') ].flatten().invoke('update', r.localdate);
-
-        // Add from/to/cc headers
-        [ 'from', 'to', 'cc' ].each(function(a) {
-            if (r[a]) {
-                (a == 'from' ? pm.select('.' + a) : [ t.down('.' + a) ]).each(function(elt) {
-                    elt.replace(DimpCore.buildAddressLinks(r[a], elt.cloneNode(false)));
-                });
-            }
-            [ $('msgHeader' + a.capitalize()) ].invoke(r[a] ? 'show' : 'hide');
-        });
-
-        // Add attachment information
-        if (r.atc_label) {
-            $('msgAtc').show();
-            tmp = $('partlist');
-            tmp.hide().previous().update(new Element('SPAN', { className: 'atcLabel' }).insert(r.atc_label)).insert(r.atc_download);
-            if (r.atc_list) {
-                $('partlist_col').show();
-                $('partlist_exp').hide();
-                tmp.down('TABLE').update(r.atc_list);
-            }
-        } else {
-            $('msgAtc').hide();
-        }
-
-        // Add message information
-        if (r.log) {
-            this.updateMsgLog(r.log);
-        } else {
-            $('msgLogInfo').hide();
-        }
-
-        $('messageBody').update(r.msgtext);
-        this.loadingImg('msg', false);
-        $('previewInfo').hide();
-        $('previewPane').scrollTop = 0;
-        pm.show();
-
-        if (r.js) {
-            eval(r.js.join(';'));
-        }
-
-        location.hash = encodeURIComponent('msg:' + row.view + ':' + row.imapuid);
-    },
-
-    _stripAttachmentCallback: function(r)
-    {
-        // Let the normal viewport refresh code and preview display code
-        // handle replacing the current preview. Set preview_replace to
-        // prevent a refresh flicker, since viewport refreshing would normally
-        // cause the preview pane to be cleared.
-        if (DimpCore.inAjaxCallback) {
-            this.preview_replace = true;
-            this.uid = r.response.newuid;
-            this._stripAttachmentCallback.bind(this, r).defer();
-            return;
-        }
-
-        this.preview_replace = false;
-
-        // Remove old cache value.
-        this._expirePPCache([ this._getPPId(r.olduid, r.oldmbox) ]);
-    },
-
-    // opts = mailbox, uid
-    updateMsgLog: function(log, opts)
-    {
-        var tmp;
-
-        if (!opts ||
-            (this.pp &&
-             this.pp.imapuid == opts.uid &&
-             this.pp.view == opts.mailbox)) {
-            $('msgLogInfo').show();
-
-            if (opts) {
-                $('msgloglist_col').show();
-                $('msgloglist_exp').hide();
-            }
-
-            DimpCore.updateMsgLog(log);
-        }
-
-        if (opts) {
-            tmp = this._getPPId(opts.uid, opts.mailbox);
-            if (this.ppcache[tmp]) {
-                this.ppcache[tmp].response.log = log;
-            }
-        }
-    },
-
-    initPreviewPane: function()
-    {
-        var sel = this.viewport.getSelected();
-        if (sel.size() != 1) {
-            this.clearPreviewPane();
-        } else {
-            this.loadPreview(sel.get('dataob').first());
-        }
-    },
-
-    clearPreviewPane: function()
-    {
-        this.loadingImg('msg', false);
-        $('previewMsg').hide();
-        $('previewPane').scrollTop = 0;
-        $('previewInfo').show();
-        this.pp = null;
-    },
-
-    _toggleHeaders: function(elt, update)
-    {
-        if (update) {
-            DIMP.conf.toggle_pref = !DIMP.conf.toggle_pref;
-            this._updatePrefs('dimp_toggle_headers', Number(elt.id == 'th_expand'));
-        }
-        [ elt.up().select('A'), $('msgHeadersColl', 'msgHeaders') ].flatten().invoke('toggle');
-    },
-
-    _expirePPCache: function(ids)
-    {
-        this.ppfifo = this.ppfifo.diff(ids);
-        ids.each(function(i) {
-            delete this.ppcache[i];
-        }, this);
-
-        if (this.ppfifo.size() > this.ppcachesize) {
-            delete this.ppcache[this.ppfifo.shift()];
-        }
-    },
-
-    _getPPId: function(uid, mailbox)
-    {
-        return uid + '|' + mailbox;
-    },
-
-    // Labeling functions
-    updateSeenUID: function(r, setflag)
-    {
-        var isunseen = !this.hasFlag('\\seen', r),
-            sel, unseen;
-
-        if ((setflag && !isunseen) || (!setflag && isunseen)) {
-            return false;
-        }
-
-        sel = this.viewport.createSelection('dataob', r);
-        unseen = this.getUnseenCount(r.view);
-
-        unseen += setflag ? -1 : 1;
-        this.updateFlag(sel, '\\seen', setflag);
-
-        this.updateUnseenStatus(r.view, unseen);
-    },
-
-    // mbox = (string)
-    getUnseenCount: function(mbox)
-    {
-        var elt = $(this.getFolderId(mbox));
-        return elt ? Number(elt.retrieve('u')) : 0;
-    },
-
-    updateUnseenStatus: function(mbox, unseen)
-    {
-        if (this.viewport) {
-            this.viewport.setMetaData({ unseen: unseen }, mbox);
-        }
-
-        this.setFolderLabel(mbox, unseen);
-
-        if (this.folder == mbox) {
-            this.updateTitle();
-        }
-    },
-
-    setMessageListTitle: function()
-    {
-        var range,
-            rows = this.viewport.getMetaData('total_rows');
-
-        if (rows) {
-            range = this.viewport.currentViewableRange();
-            $('msgHeader').update(DIMP.text.messagetitle.sub('%d', range.first).sub('%d', range.last).sub('%d', rows));
-        } else {
-            $('msgHeader').update(DIMP.text.nomessages);
-        }
-    },
-
-    // f = (string|Element)
-    setFolderLabel: function(f, unseen)
-    {
-        var elt, mbox;
-
-        if (Object.isElement(f)) {
-            mbox = f.retrieve('mbox');
-            elt = f;
-        } else {
-            mbox = f;
-            elt = $(this.getFolderId(f));
-        }
-
-        if (!elt) {
-            return;
-        }
-
-        if (Object.isUndefined(unseen)) {
-            unseen = this.getUnseenCount(mbox);
-        } else {
-            if (Object.isUndefined(elt.retrieve('u')) ||
-                elt.retrieve('u') == unseen) {
-                return;
-            }
-
-            unseen = Number(unseen);
-            elt.store('u', unseen);
-        }
-
-        if (mbox == 'INBOX' && window.fluid) {
-            window.fluid.setDockBadge(unseen ? unseen : '');
-        }
-
-        elt.down('A').update((unseen > 0) ?
-            new Element('STRONG').insert(elt.retrieve('l')).insert('&nbsp;').insert(new Element('SPAN', { className: 'count', dir: 'ltr' }).insert('(' + unseen + ')')) :
-            elt.retrieve('l'));
-    },
-
-    getFolderId: function(f)
-    {
-        return 'fld' + f.gsub('_', '__').gsub(/\W/, '_');
-    },
-
-    getSubFolderId: function(f)
-    {
-        if (f.endsWith('_special')) {
-            f = f.slice(0, -8);
-        }
-        return 'sub_' + f;
-    },
-
-    /* Folder list updates. */
-    poll: function(force)
-    {
-        var args = {},
-            check = 'checkmaillink';
-
-        // Reset poll folder counter.
-        this.setPoll();
-
-        // Check for label info - it is possible that the mailbox may be
-        // loading but not complete yet and sending this request will cause
-        // duplicate info to be returned.
-        if (this.folder &&
-            $('dimpmain_folder').visible() &&
-            this.viewport.getMetaData('label')) {
-            args = this.viewport.addRequestParams({});
-        }
-
-        if (force) {
-            args.set('forceUpdate', 1);
-            check = 'refreshlink';
-        }
-
-        $(check).down('A').update('[' + DIMP.text.check + ']');
-        DimpCore.doAction('poll', args);
-    },
-
-    pollCallback: function(r)
-    {
-        if (r.poll) {
-            $H(r.poll).each(function(u) {
-                this.updateUnseenStatus(u.key, u.value);
-            }, this);
-        }
-
-        if (r.quota) {
-            this._displayQuota(r.quota);
-        }
-
-        $('checkmaillink').down('A').update(DIMP.text.getmail);
-        if ($('refreshlink').visible()) {
-            $('refreshlink').down('A').update(DIMP.text.refresh);
-        }
-    },
-
-    _displayQuota: function(r)
-    {
-        var q = $('quota').cleanWhitespace();
-        q.setText(r.m);
-        q.down('SPAN.used IMG').writeAttribute('width', 99 - r.p);
-    },
-
-    setPoll: function()
-    {
-        if (DIMP.conf.refresh_time) {
-            if (this.pollPE) {
-                this.pollPE.stop();
-            }
-            // Run in anonymous function, or else PeriodicalExecuter passes
-            // in itself as first ('force') parameter to poll().
-            this.pollPE = new PeriodicalExecuter(function() { this.poll(); }.bind(this), DIMP.conf.refresh_time);
-        }
-    },
-
-    _portalCallback: function(r)
-    {
-        if (r.response.linkTags) {
-            var head = $(document.documentElement).down('HEAD');
-            r.response.linkTags.each(function(newLink) {
-                var link = new Element('LINK', { type: 'text/css', rel: 'stylesheet', href: newLink.href });
-                if (newLink.media) {
-                    link.media = newLink.media;
-                }
-                head.insert(link);
-            });
-        }
-        $('dimpmain_portal').update(r.response.portal);
-    },
-
-    /* Search functions. */
-    isSearch: function(id, qsearch)
-    {
-        id = id ? id : this.folder;
-        return id && id.startsWith(DIMP.conf.searchprefix) && (!qsearch || this.search);
-    },
-
-    _quicksearchOnBlur: function()
-    {
-        $('qsearch').removeClassName('qsearchFocus');
-        if (!$F('qsearch_input')) {
-            this._setQsearchText(true);
-        }
-    },
-
-    quicksearchRun: function()
-    {
-        var q = $F('qsearch_input');
-
-        if (this.isSearch()) {
-            /* Search text has changed. */
-            if (this.search.query != q) {
-                this.folderswitch = true;
-            }
-            this.viewport.reload();
-        } else {
-            this.search = {
-                label: this.viewport.getMetaData('label'),
-                mbox: this.folder,
-                query: q
-            };
-            this.loadMailbox(DIMP.conf.qsearchid);
-        }
-    },
-
-    // 'noload' = (boolean) If true, don't load the mailbox
-    quicksearchClear: function(noload)
-    {
-        var f = this.folder;
-
-        if (!$('qsearch').hasClassName('qsearchFocus')) {
-            this._setQsearchText(true);
-        }
-
-        if (this.isSearch()) {
-            this.resetSelected();
-            $('qsearch', 'qsearch_icon', 'qsearch_input').invoke('show');
-            if (!noload) {
-                this.loadMailbox(this.search ? this.search.mbox : 'INBOX');
-            }
-            this.viewport.deleteView(f);
-            this.search = null;
-        }
-    },
-
-    // d = (boolean) Deactivate quicksearch input?
-    _setQsearchText: function(d)
-    {
-        $('qsearch_input').setValue(d ? DIMP.text.search + ' (' + $('ctx_qsearchby_' + DIMP.conf.qsearchfield).getText() + ')' : '');
-        [ $('qsearch') ].invoke(d ? 'removeClassName' : 'addClassName', 'qsearchActive');
-        if ($('qsearch_input').visible()) {
-            $('qsearch_close').hide().next().hide();
-        }
-    },
-
-    // hideall = (boolean) Hide entire searchbox?
-    _quicksearchDeactivate: function(hideall)
-    {
-        if (hideall) {
-            $('qsearch').hide();
-        } else {
-            $('qsearch_close').show().next().show();
-            $('qsearch_icon', 'qsearch_input').invoke('hide');
-        }
-    },
-
-    /* Enable/Disable DIMP action buttons as needed. */
-    toggleButtons: function()
-    {
-        DimpCore.toggleButtons($('dimpmain_folder_top').select('DIV.dimpActions A.noselectDisable'), this.selectedCount() == 0);
-    },
-
-    /* Drag/Drop handler. */
-    folderDropHandler: function(e)
-    {
-        var dropbase, sel, uids,
-            drag = e.memo.element,
-            drop = e.element(),
-            foldername = drop.retrieve('mbox'),
-            ftype = drop.retrieve('ftype');
-
-        if (drag.hasClassName('folder')) {
-            dropbase = (drop == $('dropbase'));
-            if (dropbase ||
-                (ftype != 'special' && !this.isSubfolder(drag, drop))) {
-                DimpCore.doAction('renameMailbox', { old_name: drag.retrieve('mbox'), new_parent: dropbase ? '' : foldername, new_name: drag.retrieve('l') }, { callback: this.mailboxCallback.bind(this) });
-            }
-        } else if (ftype != 'container') {
-            sel = this.viewport.getSelected();
-
-            if (sel.size()) {
-                // Dragging multiple selected messages.
-                uids = sel;
-            } else if (drag.retrieve('mbox') != foldername) {
-                // Dragging a single unselected message.
-                uids = this.viewport.createSelection('domid', drag.id);
-            }
-
-            if (uids.size()) {
-                if (e.memo.dragevent.ctrlKey) {
-                    DimpCore.doAction('copyMessages', this.viewport.addRequestParams({ mboxto: foldername }), { uids: uids });
-                } else if (this.folder != foldername) {
-                    // Don't allow drag/drop to the current folder.
-                    this.updateFlag(uids, '\\deleted', true);
-                    DimpCore.doAction('moveMessages', this.viewport.addRequestParams({ mboxto: foldername }), { uids: uids });
-                }
-            }
-        }
-    },
-
-    dragCaption: function()
-    {
-        var cnt = this.selectedCount();
-        return cnt + ' ' + (cnt == 1 ? DIMP.text.message : DIMP.text.messages);
-    },
-
-    onDragMouseDown: function(e)
-    {
-        var args,
-            elt = e.element(),
-            id = elt.identify(),
-            d = DragDrop.Drags.getDrag(id);
-
-        if (elt.hasClassName('vpRow')) {
-            args = { right: e.memo.isRightClick() };
-            d.selectIfNoDrag = false;
-
-            // Handle selection first.
-            if (DimpCore.DMenu.operaCheck(e)) {
-                if (!this.isSelected('domid', id)) {
-                    this.msgSelect(id, { right: true });
-                }
-            } else if (!args.right && (e.memo.ctrlKey || e.memo.metaKey)) {
-                this.msgSelect(id, $H({ ctrl: true }).merge(args).toObject());
-            } else if (e.memo.shiftKey) {
-                this.msgSelect(id, $H({ shift: true }).merge(args).toObject());
-            } else if (e.memo.element().hasClassName('msCheck')) {
-                this.msgSelect(id, { ctrl: true, right: true });
-            } else if (this.isSelected('domid', id)) {
-                if (!args.right && this.selectedCount()) {
-                    d.selectIfNoDrag = true;
-                }
-            } else {
-                this.msgSelect(id, args);
-            }
-        } else if (elt.hasClassName('folder')) {
-            d.opera = DimpCore.DMenu.operaCheck(e);
-        }
-    },
-
-    onDrag: function(e)
-    {
-        if (e.element().hasClassName('folder')) {
-            var d = e.memo;
-            if (!d.opera && !d.wasDragged) {
-                $('folderopts').hide();
-                $('dropbase').show();
-                d.ghost.removeClassName('on');
-            }
-        }
-    },
-
-    onDragEnd: function(e)
-    {
-        var elt = e.element(),
-            id = elt.identify(),
-            d = DragDrop.Drags.getDrag(id);
-
-        if (elt.hasClassName('folder')) {
-            if (!d.opera) {
-                $('folderopts').show();
-                $('dropbase').hide();
-            }
-        } else if (elt.hasClassName('splitBarVertSidebar')) {
-            $('sidebar').setStyle({ width: d.lastCoord[0] + 'px' });
-            elt.setStyle({ left: $('sidebar').clientWidth + 'px' });
-            $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + elt.clientWidth) + 'px' });
-        }
-    },
-
-    onDragMouseUp: function(e)
-    {
-        var elt = e.element(),
-            id = elt.identify();
-
-        if (elt.hasClassName('vpRow') &&
-            DragDrop.Drags.getDrag(id).selectIfNoDrag) {
-            this.msgSelect(id, { right: e.memo.isRightClick() });
-        }
-    },
-
-    /* Keydown event handler */
-    keydownHandler: function(e)
-    {
-        var all, cnt, co, form, h, need, pp, ps, r, row, rownum, rowoff, sel,
-            tmp, vsel,
-            elt = e.element(),
-            kc = e.keyCode || e.charCode;
-
-        // Only catch keyboard shortcuts in message list view.
-        if (!$('dimpmain_folder').visible()) {
-            return;
-        }
-
-        // Form catching - normally we will ignore, but certain cases we want
-        // to catch.
-        form = e.findElement('FORM');
-        if (form) {
-            switch (kc) {
-            case Event.KEY_ESC:
-            case Event.KEY_TAB:
-                // Catch escapes in search box
-                if (elt.readAttribute('id') == 'qsearch_input') {
-                    if (kc == Event.KEY_ESC || !elt.getValue()) {
-                        this.quicksearchClear();
-                    }
-                    elt.blur();
-                    e.stop();
-                }
-                break;
-
-            case Event.KEY_RETURN:
-                // Catch returns in RedBox
-                if (form.readAttribute('id') == 'RB_folder') {
-                    this.cfolderaction(e);
-                    e.stop();
-                } else if (elt.readAttribute('id') == 'qsearch_input') {
-                    if ($F('qsearch_input')) {
-                        this.quicksearchRun();
-                    } else {
-                        this.quicksearchClear();
-                    }
-                    e.stop();
-                }
-                break;
-
-            default:
-                if (elt.readAttribute('id') == 'qsearch_input') {
-                    $('qsearch_close').show();
-                }
-                break;
-            }
-
-            return;
-        }
-
-        sel = this.viewport.getSelected();
-
-        switch (kc) {
-        case Event.KEY_DELETE:
-        case Event.KEY_BACKSPACE:
-            r = sel.get('dataob');
-            if (e.shiftKey) {
-                this.moveSelected((r.last().VP_rownum == this.viewport.getMetaData('total_rows')) ? (r.first().VP_rownum - 1) : (r.last().VP_rownum + 1), true);
-            }
-            this.deleteMsg({ vs: sel });
-            e.stop();
-            break;
-
-        case Event.KEY_UP:
-        case Event.KEY_DOWN:
-            if (e.shiftKey && this.lastrow != -1) {
-                row = this.viewport.createSelection('rownum', this.lastrow + ((kc == Event.KEY_UP) ? -1 : 1));
-                if (row.size()) {
-                    row = row.get('dataob').first();
-                    this.viewport.scrollTo(row.VP_rownum);
-                    this.msgSelect(row.VP_domid, { shift: true });
-                }
-            } else {
-                this.moveSelected(kc == Event.KEY_UP ? -1 : 1);
-            }
-            e.stop();
-            break;
-
-        case Event.KEY_PAGEUP:
-        case Event.KEY_PAGEDOWN:
-            if (e.altKey) {
-                pp = $('previewPane');
-                h = pp.getHeight();
-                if (h != pp.scrollHeight) {
-                    switch (kc) {
-                    case Event.KEY_PAGEUP:
-                        pp.scrollTop = Math.max(pp.scrollTop - h, 0);
-                        break;
-
-                    case Event.KEY_PAGEDOWN:
-                        pp.scrollTop = Math.min(pp.scrollTop + h, pp.scrollHeight - h + 1);
-                        break;
-                    }
-                }
-                e.stop();
-            } else if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
-                ps = this.viewport.getPageSize() - 1;
-                move = ps * (kc == Event.KEY_PAGEUP ? -1 : 1);
-                if (sel.size() == 1) {
-                    co = this.viewport.currentOffset();
-                    rowoff = sel.get('rownum').first() - 1;
-                    switch (kc) {
-                    case Event.KEY_PAGEUP:
-                        if (co != rowoff) {
-                            move = co - rowoff;
-                        }
-                        break;
-
-                    case Event.KEY_PAGEDOWN:
-                        if ((co + ps) != rowoff) {
-                            move = co + ps - rowoff;
-                        }
-                        break;
-                    }
-                }
-                this.moveSelected(move);
-                e.stop();
-            }
-            break;
-
-        case Event.KEY_HOME:
-        case Event.KEY_END:
-            this.moveSelected(kc == Event.KEY_HOME ? 1 : this.viewport.getMetaData('total_rows'), true);
-            e.stop();
-            break;
-
-        case Event.KEY_RETURN:
-            if (!elt.match('input')) {
-                // Popup message window if single message is selected.
-                if (sel.size() == 1) {
-                    this.msgWindow(sel.get('dataob').first());
-                }
-            }
-            e.stop();
-            break;
-
-        case 65: // A
-        case 97: // a
-            if (e.ctrlKey) {
-                this.selectAll();
-                e.stop();
-            }
-            break;
-
-        case 78: // N
-        case 110: // n
-            if (e.shiftKey && !this.isSearch(this.folder)) {
-                cnt = this.getUnseenCount(this.folder);
-                if (Object.isUndefined(cnt) || cnt) {
-                    vsel = this.viewport.getSelection();
-                    row = vsel.search({ flag: { include: '\\seen' } }).get('rownum');
-                    all = (vsel.size() == this.viewport.getMetaData('total_rows'));
-
-                    if (all ||
-                        (!Object.isUndefined(cnt) && row.size() == cnt)) {
-                        // Here we either have the entire mailbox in buffer,
-                        // or all unseen messages are in the buffer.
-                        if (sel.size()) {
-                            tmp = sel.get('rownum').last();
-                            if (tmp) {
-                                rownum = row.detect(function(r) {
-                                    return tmp < r;
-                                });
-                            }
-                        } else {
-                            rownum = tmp = row.first();
-                        }
-                    } else {
-                        // Here there is no guarantee that the next unseen
-                        // message will appear in the current buffer. Need to
-                        // determine if any gaps are between last selected
-                        // message and next unseen message in buffer.
-                        vsel = vsel.get('rownum');
-
-                        if (sel.size()) {
-                            // We know that the selected rows are in the
-                            // buffer.
-                            tmp = sel.get('rownum').last();
-                        } else if (vsel.include(1)) {
-                            // If no selected rows, start searching from the
-                            // first entry.
-                            tmp = 0;
-                        } else {
-                            // First message is not in current buffer.
-                            need = true;
-                        }
-
-                        if (!need) {
-                            rownum = vsel.detect(function(r) {
-                                if (r > tmp) {
-                                    if (++tmp != r) {
-                                        // We have found a gap.
-                                        need = true;
-                                        throw $break;
-                                    }
-                                    return row.include(tmp);
-                                }
-                            });
-
-                            if (!need && !rownum) {
-                                need = (tmp !== this.viewport.getMetaData('total_rows'));
-                            }
-                        }
-
-                        if (need) {
-                            this.viewport.select(null, { search: { unseen: 1 } });
-                        }
-                    }
-
-                    if (rownum) {
-                        this.moveSelected(rownum, true);
-                    }
-                }
-                e.stop();
-            }
-            break;
-        }
-    },
-
-    dblclickHandler: function(e)
-    {
-        if (e.isRightClick()) {
-            return;
-        }
-
-        var elt = e.element(),
-            tmp;
-
-        if (!elt.hasClassName('vpRow')) {
-            elt = elt.up('.vpRow');
-        }
-
-        if (elt) {
-            tmp = this.viewport.createSelection('domid', elt.identify()).get('dataob').first();
-            if (tmp.draft && this.viewport.getMetaData('drafts')) {
-                DimpCore.compose('resume', { folder: tmp.view, uid: tmp.imapuid })
-            } else {
-                this.msgWindow(tmp);
-            }
-            e.stop();
-        }
-    },
-
-    clickHandler: function(parentfunc, e)
-    {
-        if (e.isRightClick() || DimpCore.DMenu.operaCheck(e)) {
-            return;
-        }
-
-        var elt = e.element(),
-            id, tmp;
-
-        while (Object.isElement(elt)) {
-            id = elt.readAttribute('id');
-
-            switch (id) {
-            case 'normalfolders':
-            case 'specialfolders':
-                this._handleFolderMouseClick(e);
-                break;
-
-            case 'hometab':
-            case 'logolink':
-                this.go('portal');
-                e.stop();
-                return;
-
-            case 'button_compose':
-            case 'composelink':
-                DimpCore.compose('new');
-                e.stop();
-                return;
-
-            case 'checkmaillink':
-            case 'refreshlink':
-                this.poll(id == 'refreshlink');
-                e.stop();
-                return;
-
-            case 'alertsloglink':
-                DimpCore.Growler.toggleLog();
-                $('alertsloglink').down('A').update(DimpCore.Growler.logVisible() ? DIMP.text.hidealog : DIMP.text.showalog);
-                break;
-
-            case 'applyfilterlink':
-                if (this.viewport) {
-                    this.viewport.reload({ applyfilter: 1 });
-                }
-                e.stop();
-                return;
-
-            case 'appportal':
-            case 'appoptions':
-                this.go(id.substring(3));
-                e.stop();
-                return;
-
-            case 'applogout':
-                elt.down('A').update('[' + DIMP.text.onlogout + ']');
-                DimpCore.logout();
-                e.stop();
-                return;
-
-            case 'button_forward':
-            case 'button_reply':
-                this.composeMailbox(id == 'button_reply' ? 'reply_auto' : 'forward_auto');
-                break;
-
-            case 'button_ham':
-            case 'button_spam':
-                this.reportSpam(id == 'button_spam');
-                e.stop();
-                return;
-
-            case 'button_deleted':
-                this.deleteMsg();
-                e.stop();
-                return;
-
-            case 'msglistHeader':
-                this.sort(e.element().retrieve('sortby'));
-                e.stop();
-                return;
-
-            case 'th_expand':
-            case 'th_collapse':
-                this._toggleHeaders(elt, true);
-                break;
-
-            case 'msgloglist_toggle':
-            case 'partlist_toggle':
-                tmp = (id == 'partlist_toggle') ? 'partlist' : 'msgloglist';
-                $(tmp + '_col', tmp + '_exp').invoke('toggle');
-                Effect.toggle(tmp, 'blind', {
-                    duration: 0.2,
-                    queue: {
-                        position: 'end',
-                        scope: tmp,
-                        limit: 2
-                    }
-                });
-                break;
-
-            case 'msg_newwin':
-            case 'msg_newwin_options':
-                this.msgWindow(this.viewport.getSelection().search({ imapuid: { equal: [ this.pp.imapuid ] } , view: { equal: [ this.pp.view ] } }).get('dataob').first());
-                e.stop();
-                return;
-
-            case 'msg_view_source':
-                DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: this.pp.imapuid, mailbox: this.pp.view, actionID: 'view_source', id: 0 }, true), this.pp.imapuid + '|' + this.pp.view);
-                break;
-
-            case 'applicationfolders':
-                tmp = e.element();
-                if (!tmp.hasClassName('custom')) {
-                    tmp = tmp.up('LI.custom');
-                }
-                if (tmp) {
-                    this.go('app:' + tmp.down('A').identify().substring(3));
-                    e.stop();
-                    return;
-                }
-                break;
-
-            case 'tabbar':
-                if (e.element().hasClassName('applicationtab')) {
-                    this.go('app:' + e.element().identify().substring(6));
-                    e.stop();
-                    return;
-                }
-                break;
-
-            case 'dimpmain_portal':
-                if (e.element().match('H1.header a')) {
-                    this.go('app:' + e.element().readAttribute('app'));
-                    e.stop();
-                    return;
-                }
-                break;
-
-            case 'qsearch':
-                if (e.element().readAttribute('id') != 'qsearch_icon') {
-                    elt.addClassName('qsearchFocus');
-                    if (!elt.hasClassName('qsearchActive')) {
-                        this._setQsearchText(false);
-                    }
-                    $('qsearch_input').focus();
-                }
-                break;
-
-            case 'qsearch_close':
-            case 'qsearch_close_filter':
-                this.quicksearchClear();
-                e.stop();
-                return;
-
-            default:
-                if (elt.hasClassName('RBFolderOk')) {
-                    this.cfolderaction(e);
-                    e.stop();
-                    return;
-                } else if (elt.hasClassName('RBFolderCancel')) {
-                    this._closeRedBox();
-                    e.stop();
-                    return;
-                } else if (elt.hasClassName('printAtc')) {
-                    DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: this.pp.imapuid, mailbox: this.pp.view, actionID: 'print_attach', id: elt.readAttribute('mimeid') }, true), this.pp.imapuid + '|' + this.pp.view + '|print', IMP.printWindow);
-                    e.stop();
-                    return;
-                } else if (elt.hasClassName('stripAtc')) {
-                    this.loadingImg('msg', true);
-                    DimpCore.doAction('stripAttachment', this.viewport.addRequestParams({ id: elt.readAttribute('mimeid') }), { uids: this.viewport.createSelection('dataob', this.pp), callback: this._stripAttachmentCallback.bind(this) });
-                    e.stop();
-                    return;
-                }
-            }
-
-            elt = elt.up();
-        }
-
-        parentfunc(e);
-    },
-
-    mouseoverHandler: function(e)
-    {
-        if (DragDrop.Drags.drag) {
-            var elt = e.element();
-            if (elt.hasClassName('exp')) {
-                this._toggleSubFolder(elt.up(), 'tog');
-            }
-        }
-    },
-
-    changeHandler: function(e)
-    {
-        var elt = e.element();
-
-        if (elt.readAttribute('name') == 'search_criteria' &&
-            elt.descendantOf('RB_window')) {
-            [ elt.next() ].invoke($F(elt) ? 'show' : 'hide');
-            RedBox.setWindowPosition();
-        }
-    },
-
-    /* Handle rename folder actions. */
-    renameFolder: function(folder)
-    {
-        if (Object.isUndefined(folder)) {
-            return;
-        }
-
-        folder = $(folder);
-        var n = this._createFolderForm(this._folderAction.bindAsEventListener(this, folder, 'rename'), DIMP.text.rename_prompt);
-        n.down('input').setValue(folder.retrieve('l'));
-    },
-
-    /* Handle insert folder actions. */
-    createBaseFolder: function()
-    {
-        this._createFolderForm(this._folderAction.bindAsEventListener(this, '', 'create'), DIMP.text.create_prompt);
-    },
-
-    createSubFolder: function(folder)
-    {
-        if (!Object.isUndefined(folder)) {
-            this._createFolderForm(this._folderAction.bindAsEventListener(this, $(folder), 'createsub'), DIMP.text.createsub_prompt);
-        }
-    },
-
-    _createFolderForm: function(action, text)
-    {
-        var n = $($('folderform').down().cloneNode(true)).writeAttribute('id', 'RB_folder');
-        n.down('P').insert(text);
-
-        this.cfolderaction = action;
-
-        RedBox.overlay = true;
-        RedBox.onDisplay = Form.focusFirstElement.curry(n);
-        RedBox.showHtml(n);
-        return n;
-    },
-
-    _closeRedBox: function()
-    {
-        RedBox.close();
-        this.cfolderaction = null;
-    },
-
-    _folderAction: function(e, folder, mode)
-    {
-        this._closeRedBox();
-
-        var action, params, val,
-            form = e.findElement('form');
-        val = $F(form.down('input'));
-
-        if (val) {
-            switch (mode) {
-            case 'rename':
-                folder = folder.up('LI');
-                if (folder.retrieve('l') != val) {
-                    action = 'renameMailbox';
-                    params = {
-                        old_name: folder.retrieve('mbox'),
-                        new_parent: folder.up().hasClassName('folderlist') ? '' : folder.up(1).previous().retrieve('mbox'),
-                        new_name: val
-                    };
-                }
-                break;
-
-            case 'create':
-            case 'createsub':
-                action = 'createMailbox';
-                params = { mbox: val };
-                if (mode == 'createsub') {
-                    params.parent = folder.up('LI').retrieve('mbox');
-                }
-                break;
-            }
-
-            if (action) {
-                DimpCore.doAction(action, params, { callback: this.mailboxCallback.bind(this) });
-            }
-        }
-    },
-
-    /* Mailbox action callback functions. */
-    mailboxCallback: function(r)
-    {
-        r = r.response.mailbox;
-
-        if (r.d) {
-            r.d.each(this.deleteFolder.bind(this));
-        }
-        if (r.c) {
-            r.c.each(this.changeFolder.bind(this));
-        }
-        if (r.a) {
-            r.a.each(this.createFolder.bind(this));
-        }
-    },
-
-    deleteCallback: function(r)
-    {
-        var search = null, uids = [], vs;
-
-        if (!r.deleted) {
-            return;
-        }
-
-        this.loadingImg('viewport', false);
-
-        r = r.deleted;
-        if (!r.uids || r.mbox != this.folder) {
-            return;
-        }
-        r.uids = DimpCore.parseRangeString(r.uids);
-
-        // Need to convert uid list to listing of unique viewport IDs since
-        // we may be dealing with multiple mailboxes (i.e. virtual folders)
-        vs = this.viewport.getSelection(this.folder);
-        if (vs.getBuffer().getMetaData('search')) {
-            $H(r.uids).each(function(pair) {
-                pair.value.each(function(v) {
-                    uids.push(pair.key + DIMP.conf.IDX_SEP + v);
-                });
-            });
-
-            search = this.viewport.getSelection().search({ VP_id: { equal: uids } });
-        } else {
-            r.uids = r.uids[this.folder];
-            r.uids.each(function(f, u) {
-                uids.push(u + f);
-            }.curry(this.folder));
-            search = this.viewport.createSelection('uid', r.uids);
-        }
-
-        if (search.size()) {
-            if (r.remove) {
-                this.viewport.remove(search, { noupdate: r.ViewPort });
-                this._expirePPCache(uids);
-            } else {
-                // Need this to catch spam deletions.
-                this.updateFlag(search, '\\deleted', true);
-            }
-        }
-    },
-
-    _emptyMailboxCallback: function(r)
-    {
-        if (r.response.mbox) {
-            if (this.folder == r.response.mbox) {
-                this.viewport.reload();
-                this.clearPreviewPane();
-            } else {
-                this.viewport.deleteView(r.response.mbox);
-            }
-            this.setFolderLabel(r.response.mbox, 0);
-        }
-    },
-
-    _flagAllCallback: function(r)
-    {
-        if (r.response &&
-            r.response.mbox == this.folder) {
-            r.response.flags.each(function(f) {
-                this.updateFlag(this.viewport.createSelection('rownum', this.viewport.getAllRows()), f, r.response.set);
-            }, this);
-        }
-    },
-
-    _folderLoadCallback: function(r, callback)
-    {
-        this.mailboxCallback(r);
-
-        if (callback) {
-            callback();
-        }
-
-        if (this.folder) {
-            this.highlightSidebar(this.getFolderId(this.folder));
-        }
-
-        $('foldersLoading').hide();
-        $('foldersSidebar').show();
-
-        if ($('normalfolders').getStyle('max-height') !== null) {
-            this._sizeFolderlist();
-        }
-
-        if (r.response.quota) {
-            this._displayQuota(r.response.quota);
-        }
-    },
-
-    _handleFolderMouseClick: function(e)
-    {
-        var elt = e.element(),
-            li = elt.match('LI') ? elt : elt.up('LI');
-
-        if (!li) {
-            return;
-        }
-
-        if (elt.hasClassName('exp') || elt.hasClassName('col')) {
-            this._toggleSubFolder(li, 'tog');
-        } else {
-            switch (li.retrieve('ftype')) {
-            case 'container':
-            case 'scontainer':
-                e.stop();
-                break;
-
-            case 'folder':
-            case 'special':
-            case 'virtual':
-                e.stop();
-                return this.go('folder:' + li.retrieve('mbox'));
-            }
-        }
-    },
-
-    _toggleSubFolder: function(base, mode, noeffect)
-    {
-        var need = [], subs = [];
-
-        if (mode == 'expall' || mode == 'colall') {
-            if (base.hasClassName('subfolders')) {
-                subs.push(base);
-            }
-            subs = subs.concat(base.select('.subfolders'));
-        } else if (mode == 'exp') {
-            // If we are explicitly expanding ('exp'), make sure all parent
-            // subfolders are expanded.
-            // The last 2 elements of ancestors() are the BODY and HTML tags -
-            // don't need to parse through them.
-            subs = base.ancestors().slice(0, -2).reverse().findAll(function(n) { return n.hasClassName('subfolders'); });
-        } else {
-            subs = [ base.next('.subfolders') ];
-        }
-
-        if (!subs) {
-            return;
-        }
-
-        if (mode == 'tog' || mode == 'expall') {
-            subs.compact().each(function(s) {
-                if (!s.visible() && !s.down().childElements().size()) {
-                    need.push(s.previous().retrieve('mbox'));
-                }
-            });
-
-            if (need.size()) {
-                if (mode == 'tog') {
-                    base.down('A').update(DIMP.text.loading);
-                }
-                this._listFolders({
-                    all: Number(mode == 'expall'),
-                    callback: this._toggleSubFolder.bind(this, base, mode, noeffect),
-                    mboxes: need
-                });
-                return;
-            } else if (mode == 'tog') {
-                // Need to pass element here, since we might be working
-                // with 'special' folders.
-                this.setFolderLabel(base);
-            }
-        }
-
-        subs.each(function(s) {
-            if (mode == 'tog' ||
-                ((mode == 'exp' || mode == 'expall') && !s.visible()) ||
-                ((mode == 'col' || mode == 'colall') && s.visible())) {
-                s.previous().down().toggleClassName('exp').toggleClassName('col');
-
-                if (noeffect) {
-                    s.toggle();
-                } else {
-                    Effect.toggle(s, 'blind', {
-                        duration: 0.2,
-                        queue: {
-                            position: 'end',
-                            scope: 'subfolder'
-                        }
-                    });
-                }
-            }
-        });
-    },
-
-    _listFolders: function(params)
-    {
-        var cback;
-
-        params = params || {};
-        params.unsub = Number(this.showunsub);
-        if (!Object.isArray(params.mboxes)) {
-            params.mboxes = [ params.mboxes ];
-        }
-        params.mboxes = params.mboxes.toJSON();
-
-        if (params.callback) {
-            cback = function(func, r) { this._folderLoadCallback(r, func); }.bind(this, params.callback);
-            delete params.callback;
-        } else {
-            cback = this._folderLoadCallback.bind(this);
-        }
-
-        DimpCore.doAction('listMailboxes', params, { callback: cback });
-    },
-
-    // Folder actions.
-    // For format of the ob object, see IMP_Dimp::_createFolderElt().
-    createFolder: function(ob)
-    {
-        var div, f_node, ftype, li, ll, parent_e, tmp,
-            cname = 'container',
-            fid = this.getFolderId(ob.m),
-            label = ob.l || ob.m,
-            mbox = ob.m,
-            submboxid = this.getSubFolderId(fid),
-            submbox = $(submboxid),
-            title = ob.t || ob.m;
-
-        if ($(fid)) {
-            return;
-        }
-
-        if (ob.v) {
-            ftype = ob.co ? 'scontainer' : 'virtual';
-            title = label;
-        } else if (ob.co) {
-            if (ob.n) {
-                ftype = 'scontainer';
-                title = label;
-            } else {
-                ftype = 'container';
-            }
-
-            /* This is a dummy container element to display child elements of
-             * a mailbox displayed in the 'specialfolders' section. */
-            if (ob.dummy) {
-                fid += '_special';
-                cname += ' specialContainer';
-            }
-        } else {
-            cname = 'folder';
-            ftype = ob.s ? 'special' : 'folder';
-        }
-
-        if (ob.un && this.showunsub) {
-            cname += ' unsubFolder';
-        }
-
-        div = new Element('SPAN', { className: 'iconSpan' });
-        if (ob.i) {
-            div.setStyle({ backgroundImage: 'url("' + ob.i + '")' });
-        }
-
-        li = new Element('LI', { className: cname, id: fid, title: title }).store('l', label).store('mbox', mbox).insert(div).insert(new Element('A').insert(label));
-
-        // Now walk through the parent <ul> to find the right place to
-        // insert the new folder.
-        if (submbox) {
-            if (submbox.insert({ before: li }).visible()) {
-                // If an expanded parent mailbox was deleted, we need to toggle
-                // the icon accordingly.
-                div.addClassName('col');
-            }
-        } else {
-            div.addClassName(ob.ch ? 'exp' : (ob.cl || 'folderImg'));
-
-            if (ob.s) {
-                parent_e = $('specialfolders');
-
-                /* Create a dummy container element in 'normalfolders'
-                 * section. */
-                if (ob.ch) {
-                    div.removeClassName('exp').addClassName(ob.cl || 'folderImg');
-
-                    tmp = Object.clone(ob);
-                    tmp.co = tmp.dummy = true;
-                    tmp.s = false;
-                    this.createFolder(tmp);
-                }
-            } else {
-                parent_e = ob.pa
-                    ? $(this.getSubFolderId(this.getFolderId(ob.pa))).down()
-                    : $('normalfolders');
-            }
-
-            /* Virtual folders are sorted on the server. */
-            if (!ob.v) {
-                ll = mbox.toLowerCase();
-                f_node = parent_e.childElements().find(function(node) {
-                    var nodembox = node.retrieve('mbox');
-                    return nodembox &&
-                           (!ob.s || nodembox != 'INBOX') &&
-                           (ll < nodembox.toLowerCase());
-                });
-            }
-
-            if (f_node) {
-                f_node.insert({ before: li });
-            } else {
-                parent_e.insert(li);
-            }
-
-            // Make sure the sub<mbox> ul is created if necessary.
-            if (!ob.s && ob.ch) {
-                li.insert({ after: new Element('LI', { className: 'subfolders', id: submboxid }).insert(new Element('UL')).hide() });
-            }
-        }
-
-        li.store('ftype', ftype);
-
-        // Make the new folder a drop target.
-        if (!ob.v) {
-            new Drop(li, this._folderDropConfig);
-        }
-
-        // Check for unseen messages
-        if (ob.po) {
-            li.store('u', '');
-            this.setFolderLabel(mbox, ob.u);
-        }
-
-        switch (ftype) {
-        case 'special':
-            // For purposes of the contextmenu, treat special folders
-            // like regular folders.
-            ftype = 'folder';
-            // Fall through.
-
-        case 'container':
-        case 'folder':
-            new Drag(li, this._folderDragConfig);
-            DimpCore.addContextMenu({
-                id: fid,
-                type: ftype
-            });
-            break;
-
-        case 'scontainer':
-        case 'virtual':
-            DimpCore.addContextMenu({
-                id: fid,
-                type: (ob.v == 2) ? 'vfolder' : 'noactions'
-            });
-            break;
-        }
-    },
-
-    deleteFolder: function(folder)
-    {
-        if (this.folder == folder) {
-            this.go('folder:INBOX');
-        }
-        this.deleteFolderElt(this.getFolderId(folder), true);
-    },
-
-    changeFolder: function(ob)
-    {
-        var fdiv, oldexpand,
-            fid = this.getFolderId(ob.m);
-
-        if ($(fid + '_special')) {
-            // The case of children being added to a special folder is
-            // handled by createFolder().
-            if (!ob.ch) {
-                this.deleteFolderElt(fid + '_special', true);
-            }
-            return;
-        }
-
-        fdiv = $(fid).down('DIV');
-        oldexpand = fdiv && fdiv.hasClassName('col');
-
-        this.deleteFolderElt(fid, !ob.ch);
-        if (ob.co && this.folder == ob.m) {
-            this.go('folder:INBOX');
-        }
-        this.createFolder(ob);
-        if (ob.ch && oldexpand) {
-            fdiv.removeClassName('exp').addClassName('col');
-        }
-    },
-
-    deleteFolderElt: function(fid, sub)
-    {
-        var f = $(fid), submbox;
-        if (!f) {
-            return;
-        }
-
-        if (sub) {
-            submbox = $(this.getSubFolderId(fid));
-            if (submbox) {
-                submbox.remove();
-            }
-        }
-        [ DragDrop.Drags.getDrag(fid), DragDrop.Drops.getDrop(fid) ].compact().invoke('destroy');
-        this._removeMouseEvents(f);
-        if (this.viewport) {
-            this.viewport.deleteView(fid);
-        }
-        f.remove();
-    },
-
-    _sizeFolderlist: function()
-    {
-        var nf = $('normalfolders');
-        nf.setStyle({ height: (document.viewport.getHeight() - nf.cumulativeOffset()[1]) + 'px' });
-    },
-
-    toggleSubscribed: function()
-    {
-        this.showunsub = !this.showunsub;
-        $('ctx_folderopts_sub', 'ctx_folderopts_unsub').invoke('toggle');
-        this._reloadFolders();
-    },
-
-    _reloadFolders: function()
-    {
-        $('foldersLoading').show();
-        $('foldersSidebar').hide();
-
-        [ $('specialfolders').childElements(), $('dropbase').nextSiblings() ].flatten().each(function(elt) {
-            this.deleteFolderElt(elt.readAttribute('id'), true);
-        }, this);
-
-        this._listFolders({ reload: 1, mboxes: this.folder });
-    },
-
-    subscribeFolder: function(f, sub)
-    {
-        var fid = this.getFolderId(f);
-        DimpCore.doAction('subscribe', { mbox: f, sub: Number(sub) });
-
-        if (this.showunsub) {
-            [ $(fid) ].invoke(sub ? 'removeClassName' : 'addClassName', 'unsubFolder');
-        } else if (!sub) {
-            this.deleteFolderElt(fid);
-        }
-    },
-
-    /* Flag actions for message list. */
-    _getFlagSelection: function(opts)
-    {
-        var vs;
-
-        if (opts.vs) {
-            vs = opts.vs;
-        } else if (opts.uid) {
-            vs = opts.mailbox
-                ? this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ opts.uid ] }, view: { equal: [ opts.mailbox ] } })
-                : this.viewport.createSelection('dataob', opts.uid);
-        } else {
-            vs = this.viewport.getSelected();
-        }
-
-        return vs;
-    },
-
-    _doMsgAction: function(type, opts, args)
-    {
-        var vs = this._getFlagSelection(opts);
-
-        if (vs.size()) {
-            // This needs to be synchronous Ajax if we are calling from a
-            // popup window because Mozilla will not correctly call the
-            // callback function if the calling window has been closed.
-            DimpCore.doAction(type, this.viewport.addRequestParams(args), { uids: vs, ajaxopts: { asynchronous: !(opts.uid && opts.mailbox) } });
-            return vs;
-        }
-
-        return false;
-    },
-
-    // spam = (boolean) True for spam, false for innocent
-    // opts = 'mailbox', 'uid'
-    reportSpam: function(spam, opts)
-    {
-        opts = opts || {};
-        if (this._doMsgAction('reportSpam', opts, { spam: Number(spam) })) {
-            // Indicate to the user that something is happening (since spam
-            // reporting may not be instantaneous).
-            this.loadingImg('viewport', true);
-        }
-    },
-
-    // blacklist = (boolean) True for blacklist, false for whitelist
-    // opts = 'mailbox', 'uid'
-    blacklist: function(blacklist, opts)
-    {
-        opts = opts || {};
-        this._doMsgAction('blacklist', opts, { blacklist: blacklist });
-    },
-
-    // opts = 'mailbox', 'uid'
-    deleteMsg: function(opts)
-    {
-        opts = opts || {};
-        var vs = this._getFlagSelection(opts);
-
-        // Make sure that any given row is not deleted more than once. Need to
-        // explicitly mark here because message may already be flagged deleted
-        // when we load page (i.e. switching to using trash folder).
-        vs = vs.search({ isdel: { notequal: [ true ] } });
-        if (!vs.size()) {
-            return;
-        }
-        vs.set({ isdel: true });
-
-        opts.vs = vs;
-
-        this._doMsgAction('deleteMessages', opts, {});
-        this.updateFlag(vs, '\\deleted', true);
-    },
-
-    // flag = (string) IMAP flag name
-    // set = (boolean) True to set flag
-    // opts = (Object) 'mailbox', 'noserver', 'uid'
-    flag: function(flag, set, opts)
-    {
-        opts = opts || {};
-        var flags = [ (set ? '' : '-') + flag ],
-            vs = this._getFlagSelection(opts);
-
-        if (!vs.size()) {
-            return;
-        }
-
-        switch (flag) {
-        case '\\answered':
-            if (set) {
-                this.updateFlag(vs, '\\flagged', false);
-                flags.push('-\\flagged');
-            }
-            break;
-
-        case '\\deleted':
-            vs.set({ isdel: false });
-            break;
-
-        case '\\seen':
-            vs.get('dataob').each(function(s) {
-                this.updateSeenUID(s, set);
-            }, this);
-            break;
-        }
-
-        this.updateFlag(vs, flag, set);
-        if (!opts.noserver) {
-            DimpCore.doAction('flagMessages', this.viewport.addRequestParams({ flags: flags.toJSON(), view: this.folder }), { uids: vs });
-        }
-    },
-
-    // type = (string) 'seen' or 'unseen'
-    // mbox = (string) The mailbox to flag
-    flagAll: function(type, set, mbox)
-    {
-        DimpCore.doAction('flagAll', { flags: [ type ].toJSON(), set: Number(set), mbox: mbox }, { callback: this._flagAllCallback.bind(this) });
-    },
-
-    hasFlag: function(f, r)
-    {
-        return this.convertFlag(f, r.flag ? r.flag.include(f) : false);
-    },
-
-    convertFlag: function(f, set)
-    {
-        /* For some flags, we need to do an inverse match (e.g. knowing a
-         * message is SEEN is not as important as knowing the message lacks
-         * the SEEN FLAG). This function will determine if, for a given flag,
-         * the inverse action should be taken on it. */
-        return DIMP.conf.flags[f].n ? !set : set;
-    },
-
-    updateFlag: function(vs, flag, add)
-    {
-        var s = {};
-        add = this.convertFlag(flag, add);
-
-        vs.get('dataob').each(function(ob) {
-            this._updateFlag(ob, flag, add);
-
-            if (this.isSearch()) {
-                if (s[ob.view]) {
-                    s[ob.view].push(ob.imapuid);
-                } else {
-                    s[ob.view] = [ ob.imapuid ];
-                }
-            }
-        }, this);
-
-        /* If this is a search mailbox, also need to update flag in base view,
-         * if it is in the buffer. */
-        $H(s).each(function(m) {
-            var tmp = this.viewport.getSelection(m.key).search({ imapuid: { equal: m.value }, view: { equal: m.key } });
-            if (tmp.size()) {
-                this._updateFlag(tmp.get('dataob').first(), flag, add);
-            }
-        }, this);
-    },
-
-    _updateFlag: function(ob, flag, add)
-    {
-        ob.flag = ob.flag
-            ? ob.flag.without(flag)
-            : [];
-
-        if (add) {
-            ob.flag.push(flag);
-        }
-
-        this.viewport.updateRow(ob);
-    },
-
-    /* Miscellaneous folder actions. */
-    purgeDeleted: function()
-    {
-        DimpCore.doAction('purgeDeleted', this.viewport.addRequestParams({}));
-    },
-
-    modifyPoll: function(folder, add)
-    {
-        DimpCore.doAction('modifyPoll', { add: Number(add), mbox: folder }, { callback: this._modifyPollCallback.bind(this) });
-    },
-
-    _modifyPollCallback: function(r)
-    {
-        r = r.response;
-        var f = r.mbox, fid, p = { response: { poll: {} } };
-        fid = $(this.getFolderId(f));
-
-        if (r.add) {
-            p.response.poll[f] = r.poll.u;
-            fid.store('u', 0);
-        } else {
-            p.response.poll[f] = 0;
-        }
-
-        if (!r.add) {
-            fid.store('u', null);
-            this.updateUnseenStatus(f, 0);
-        }
-    },
-
-    loadingImg: function(id, show)
-    {
-        DimpCore.loadingImg(id + 'Loading', id == 'viewport' ? 'msgSplitPane' : 'previewPane', show);
-    },
-
-    // p = (element) Parent element
-    // c = (element) Child element
-    isSubfolder: function(p, c)
-    {
-        var sf = $(this.getSubFolderId(p.identify()));
-        return sf && c.descendantOf(sf);
-    },
-
-    /* Pref updating function. */
-    _updatePrefs: function(pref, value)
-    {
-        new Ajax.Request(DimpCore.addURLParam(DIMP.conf.URI_PREFS), { parameters: { pref: pref, value: value } });
-    },
-
-    /* Onload function. */
-    onDomLoad: function()
-    {
-        DimpCore.init();
-
-        var DM = DimpCore.DMenu, tmp;
-
-        /* Register global handlers now. */
-        document.observe('keydown', this.keydownHandler.bindAsEventListener(this));
-        document.observe('change', this.changeHandler.bindAsEventListener(this));
-        document.observe('dblclick', this.dblclickHandler.bindAsEventListener(this));
-        Event.observe(window, 'resize', this.onResize.bind(this));
-
-        /* Limit to folders sidebar only. */
-        $('foldersSidebar').observe('mouseover', this.mouseoverHandler.bindAsEventListener(this));
-
-        /* Show page now. */
-        $('sidebar').setStyle({ width: DIMP.conf.sidebar_width });
-        $('dimpLoading').hide();
-        $('dimpPage').show();
-
-        /* Create splitbar for sidebar. */
-        this.splitbar = new Element('DIV', { className: 'splitBarVertSidebar' }).setStyle({ height: document.viewport.getHeight() + 'px', left: $('sidebar').clientWidth + 'px' });
-        $('sidebar').insert({ after: this.splitbar });
-        new Drag(this.splitbar, {
-            constraint: 'horizontal',
-            ghosting: true,
-            nodrop: true
-        });
-
-        $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + this.splitbar.clientWidth) + 'px' });
-
-        /* Init quicksearch. These needs to occur before loading the message
-         * list since it may be disabled if we are in a search mailbox. */
-        if ($('qsearch')) {
-            $('qsearch_input').observe('blur', this._quicksearchOnBlur.bind(this));
-            DimpCore.addContextMenu({
-                id: 'qsearch_icon',
-                left: true,
-                offset: 'qsearch',
-                type: 'qsearchopts'
-            });
-            DimpCore.addContextMenu({
-                id: 'qsearch_icon',
-                left: false,
-                offset: 'qsearch',
-                type: 'qsearchopts'
-            });
-            DM.addSubMenu('ctx_qsearchopts_by', 'ctx_qsearchby');
-            DM.addSubMenu('ctx_qsearchopts_filter', 'ctx_flag');
-            DM.addSubMenu('ctx_qsearchopts_filternot', 'ctx_flag');
-        }
-
-        /* Store these text strings for updating purposes. */
-        DIMP.text.getmail = $('checkmaillink').down('A').innerHTML;
-        DIMP.text.refresh = $('refreshlink').down('A').innerHTML;
-        DIMP.text.showalog = $('alertsloglink').down('A').innerHTML;
-
-        /* Initialize the starting page. */
-        tmp = location.hash;
-        if (!tmp.empty() && tmp.startsWith('#')) {
-            tmp = (tmp.length == 1) ? "" : tmp.substring(1);
-        }
-
-        if (!tmp.empty()) {
-            this.go(decodeURIComponent(tmp));
-        } else if (DIMP.conf.login_view == 'inbox') {
-            this.go('folder:INBOX');
-        } else {
-            this.go('portal');
-            this.loadMailbox('INBOX', { background: true });
-        }
-
-        /* Create the folder list. Any pending notifications will be caught
-         * via the return from this call. */
-        this._listFolders({ initial: 1, mboxes: this.folder} );
-
-        this._setQsearchText(true);
-
-        /* Add popdown menus. Check for disabled compose at the same time. */
-        DimpCore.addPopdown('button_other', 'otheractions', true);
-        DimpCore.addPopdown('folderopts_link', 'folderopts', true);
-
-        DM.addSubMenu('ctx_message_reply', 'ctx_reply');
-        DM.addSubMenu('ctx_message_forward', 'ctx_forward');
-        [ 'ctx_message_', 'oa_' ].each(function(i) {
-            if ($(i + 'setflag')) {
-                DM.addSubMenu(i + 'setflag', 'ctx_flag');
-                DM.addSubMenu(i + 'unsetflag', 'ctx_flag');
-            }
-        });
-        DM.addSubMenu('ctx_folder_setflag', 'ctx_folder_flag');
-
-        if (DIMP.conf.disable_compose) {
-            $('button_reply', 'button_forward').compact().invoke('up', 'SPAN').concat($('button_compose', 'composelink', 'ctx_contacts_new')).compact().invoke('remove');
-        } else {
-            DimpCore.addPopdown('button_reply', 'reply', false, true);
-            DimpCore.addPopdown('button_forward', 'forward', false, true);
-        }
-
-        DimpCore.addContextMenu({
-            id: 'msglistHeader',
-            type: 'mboxsort'
-        });
-
-        new Drop('dropbase', this._folderDropConfig);
-
-        if (DIMP.conf.toggle_pref) {
-            this._toggleHeaders($('th_expand'));
-        }
-
-        /* Remove unavailable menu items. */
-        if (!$('GrowlerLog')) {
-            $('alertsloglink').remove();
-        }
-
-        /* Check for new mail. */
-        this.setPoll();
-    },
-
-    /* Resize function. */
-    onResize: function()
-    {
-        if (this.resize) {
-            clearTimeout(this.resize);
-        }
-
-        this.resize = this._onResize.bind(this).delay(0.1);
-    },
-
-    _onResize: function()
-    {
-        this._sizeFolderlist();
-        this.splitbar.setStyle({ height: document.viewport.getHeight() + 'px' });
-    },
-
-    /* Extend AJAX exception handling. */
-    onAjaxException: function(parentfunc, r, e)
-    {
-        /* Make sure loading images are closed. */
-        this.loadingImg('msg', false);
-        this.loadingImg('viewport', false);
-        DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_error } ]);
-        parentfunc(r, e);
-    }
-
-};
-
-/* Need to add after DimpBase is defined. */
-DimpBase._msgDragConfig = {
-    classname: 'msgdrag',
-    scroll: 'normalfolders',
-    threshold: 5,
-    caption: DimpBase.dragCaption.bind(DimpBase)
-};
-
-DimpBase._folderDragConfig = {
-    classname: 'folderdrag',
-    ghosting: true,
-    offset: { x: 15, y: 0 },
-    scroll: 'normalfolders',
-    threshold: 5
-};
-
-DimpBase._folderDropConfig = {
-    caption: function(drop, drag, e) {
-        var m,
-            d = drag.retrieve('l'),
-            ftype = drop.retrieve('ftype'),
-            l = drop.retrieve('l');
-
-        if (drop == $('dropbase')) {
-            return DIMP.text.moveto.sub('%s', d).sub('%s', DIMP.text.baselevel);
-        }
-
-        switch (e.type) {
-        case 'mousemove':
-            m = (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto;
-            break;
-
-        case 'keydown':
-            /* Can't use ctrlKey here since different browsers handle the
-             * ctrlKey in different ways when it comes to firing keyboard
-             * events. */
-            m = (e.keyCode == 17) ? DIMP.text.copyto : DIMP.text.moveto;
-            break;
-
-        case 'keyup':
-            m = (e.keyCode == 17)
-                ? DIMP.text.moveto
-                : (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto;
-            break;
-        }
-
-        if (drag.hasClassName('folder')) {
-            return (ftype != 'special' && !DimpBase.isSubfolder(drag, drop)) ? m.sub('%s', d).sub('%s', l) : '';
-        }
-
-        return ftype != 'container' ? m.sub('%s', DimpBase.dragCaption()).sub('%s', l) : '';
-    },
-    keypress: true
-};
-
-/* Drag/drop listeners. */
-document.observe('DragDrop2:drag', DimpBase.onDrag.bindAsEventListener(DimpBase));
-document.observe('DragDrop2:drop', DimpBase.folderDropHandler.bindAsEventListener(DimpBase));
-document.observe('DragDrop2:end', DimpBase.onDragEnd.bindAsEventListener(DimpBase));
-document.observe('DragDrop2:mousedown', DimpBase.onDragMouseDown.bindAsEventListener(DimpBase));
-document.observe('DragDrop2:mouseup', DimpBase.onDragMouseUp.bindAsEventListener(DimpBase));
-
-/* Route AJAX responses through ViewPort. */
-DimpCore.onDoActionComplete = function(r) {
-    DimpBase.deleteCallback(r);
-    if (DimpBase.viewport) {
-        DimpBase.viewport.parseJSONResponse(r);
-    }
-    DimpBase.pollCallback(r);
-};
-
-/* Click handler. */
-DimpCore.clickHandler = DimpCore.clickHandler.wrap(DimpBase.clickHandler.bind(DimpBase));
-
-/* ContextSensitive handlers. */
-DimpCore.contextOnClick = DimpCore.contextOnClick.wrap(DimpBase.contextOnClick.bind(DimpBase));
-DimpCore.contextOnShow = DimpCore.contextOnShow.wrap(DimpBase.contextOnShow.bind(DimpBase));
-
-/* Extend AJAX exception handling. */
-DimpCore.doActionOpts.onException = DimpCore.doActionOpts.onException.wrap(DimpBase.onAjaxException.bind(DimpBase));
-
-/* Initialize onload handler. */
-document.observe('dom:loaded', DimpBase.onDomLoad.bind(DimpBase));
diff --git a/imp/js/DimpCore.js b/imp/js/DimpCore.js
deleted file mode 100644 (file)
index 04cbc29..0000000
+++ /dev/null
@@ -1,595 +0,0 @@
-/**
- * DimpCore.js - Dimp UI application logic.
- *
- * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-/* DimpCore object. */
-var DimpCore = {
-    // Vars used and defaulting to null/false:
-    //   DMenu, Growler, inAjaxCallback, is_init, is_logout
-    //   onDoActionComplete
-    alarms: {},
-    growler_log: true,
-    server_error: 0,
-
-    doActionOpts: {
-        onException: function(r, e) { DimpCore.debug('onException', e); },
-        onFailure: function(t, o) { DimpCore.debug('onFailure', t); },
-        evalJS: false,
-        evalJSON: true
-    },
-
-    debug: function(label, e)
-    {
-        if (!this.is_logout && window.console && window.console.error) {
-            window.console.error(label, Prototype.Browser.Gecko ? e : $H(e).inspect());
-        }
-    },
-
-    // Convert object to an IMP UID Range string. See IMP::toRangeString()
-    // ob = (object) mailbox name as keys, values are array of uids.
-    toRangeString: function(ob)
-    {
-        var str = '';
-
-        $H(ob).each(function(o) {
-            if (!o.value.size()) {
-                return;
-            }
-
-            var u = o.value.numericSort(),
-                first = u.shift(),
-                last = first,
-                out = [];
-
-            u.each(function(k) {
-                if (last + 1 == k) {
-                    last = k;
-                } else {
-                    out.push(first + (last == first ? '' : (':' + last)));
-                    first = last = k;
-                }
-            });
-            out.push(first + (last == first ? '' : (':' + last)));
-            str += '{' + o.key.length + '}' + o.key + out.join(',');
-        });
-
-        return str;
-    },
-
-    // Parses an IMP UID Range string. See IMP::parseRangeString()
-    // str = (string) An IMP UID range string.
-    parseRangeString: function(str)
-    {
-        var count, end, i, mbox, uidstr,
-            mlist = {},
-            uids = [];
-        str = str.strip();
-
-        while (!str.blank()) {
-            if (!str.startsWith('{')) {
-                break;
-            }
-            i = str.indexOf('}');
-            count = Number(str.substr(1, i - 1));
-            mbox = str.substr(i + 1, count);
-            i += count + 1;
-            end = str.indexOf('{', i);
-            if (end == -1) {
-                uidstr = str.substr(i);
-                str = '';
-            } else {
-                uidstr = str.substr(i, end - i);
-                str = str.substr(end);
-            }
-
-            uidstr.split(',').each(function(e) {
-                var r = e.split(':');
-                if (r.size() == 1) {
-                    uids.push(Number(e));
-                } else {
-                    uids = uids.concat($A($R(Number(r[0]), Number(r[1]))));
-                }
-            });
-
-            mlist[mbox] = uids;
-        }
-
-        return mlist;
-    },
-
-    // 'opts' -> ajaxopts, callback, uids
-    doAction: function(action, params, opts)
-    {
-        params = $H(params);
-        opts = opts || {};
-
-        var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {});
-
-        if (opts.uids) {
-            if (opts.uids.viewport_selection) {
-                opts.uids = this.selectionToRange(opts.uids);
-            }
-            params.set('uid', this.toRangeString(opts.uids));
-        }
-
-        ajaxopts.parameters = this.addRequestParams(params);
-        ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this);
-
-        new Ajax.Request(DIMP.conf.URI_AJAX + action, ajaxopts);
-    },
-
-    // 'opts' -> ajaxopts, callback
-    submitForm: function(form, opts)
-    {
-        opts = opts || {};
-        var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {});
-        ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this);
-        $(form).request(ajaxopts);
-    },
-
-    selectionToRange: function(s)
-    {
-        var b = s.getBuffer(),
-            tmp = {};
-
-        if (b.getMetaData('search')) {
-            s.get('uid').each(function(r) {
-                var parts = r.split(DIMP.conf.IDX_SEP);
-                if (tmp[parts[0]]) {
-                    tmp[parts[0]].push(parts[1]);
-                } else {
-                    tmp[parts[0]] = [ parts[1] ];
-                }
-            });
-        } else {
-            tmp[b.getView()] = s.get('uid');
-        }
-
-        return tmp;
-    },
-
-    // params - (Hash)
-    addRequestParams: function(params)
-    {
-        var p = params.clone();
-
-        if (DIMP.conf.SESSION_ID) {
-            p.update(DIMP.conf.SESSION_ID.toQueryParams());
-        }
-
-        return p;
-    },
-
-    doActionComplete: function(request, callback)
-    {
-        this.inAjaxCallback = true;
-
-        if (!request.responseJSON) {
-            if (++this.server_error == 3) {
-                this.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_timeout } ]);
-            }
-            this.inAjaxCallback = false;
-            return;
-        }
-
-        var r = request.responseJSON;
-
-        if (!r.msgs) {
-            r.msgs = [];
-        }
-
-        if (r.response && Object.isFunction(callback)) {
-            try {
-                callback(r);
-            } catch (e) {
-                this.debug('doActionComplete', e);
-            }
-        }
-
-        if (this.server_error >= 3) {
-            r.msgs.push({ type: 'horde.success', message: DIMP.text.ajax_recover });
-        }
-        this.server_error = 0;
-
-        this.showNotifications(r.msgs);
-
-        if (r.response && this.onDoActionComplete) {
-            this.onDoActionComplete(r.response);
-        }
-
-        this.inAjaxCallback = false;
-    },
-
-    setTitle: function(title)
-    {
-        document.title = DIMP.conf.name + ' :: ' + title;
-    },
-
-    showNotifications: function(msgs)
-    {
-        if (!msgs.size() || this.is_logout) {
-            return;
-        }
-
-        msgs.find(function(m) {
-            switch (m.type) {
-            case 'horde.ajaxtimeout':
-                this.logout(m.message);
-                return true;
-
-            case 'horde.alarm':
-                if (!this.alarms[m.flags.alarm.id]) {
-                    this.Growler.growl(m.flags.alarm.title + ': ' + m.flags.alarm.text, {
-                        className: 'horde-alarm',
-                        sticky: 1,
-                        log: 1
-                    });
-                    this.alarms[m.flags.alarm.id] = 1;
-                }
-                break;
-
-            case 'horde.error':
-            case 'horde.message':
-            case 'horde.success':
-            case 'horde.warning':
-                this.Growler.growl(m.message, {
-                    className: m.type.replace('.', '-'),
-                    life: (m.type == 'horde.error' ? 12 : 8),
-                    log: 1
-                });
-                break;
-
-            case 'imp.reply':
-            case 'imp.forward':
-            case 'imp.redirect':
-                this.Growler.growl(m.message, {
-                    className: m.type.replace('.', '-'),
-                    life: 8
-                });
-                break;
-            }
-        }, this);
-    },
-
-    compose: function(type, args)
-    {
-        var url = DIMP.conf.URI_COMPOSE;
-        args = args || {};
-        if (type) {
-            args.type = type;
-        }
-        this.popupWindow(this.addURLParam(url, args), 'compose' + new Date().getTime());
-    },
-
-    popupWindow: function(url, name, onload)
-    {
-        var opts = {
-            height: DIMP.conf.popup_height,
-            name: name.gsub(/\W/, '_'),
-            noalert: true,
-            onload: onload,
-            url: url,
-            width: DIMP.conf.popup_width
-        };
-
-        if (!Horde.popup(opts)) {
-            this.showNotifications([ { type: 'horde.warning', message: DIMP.text.popup_block } ]);
-        }
-    },
-
-    closePopup: function()
-    {
-        // Mozilla bug/feature: it will not close a browser window
-        // automatically if there is code remaining to be performed (or, at
-        // least, not here) unless the mouse is moved or a keyboard event
-        // is triggered after the callback is complete. (As of FF 2.0.0.3 and
-        // 1.5.0.11).  So wait for the callback to complete before attempting
-        // to close the window.
-        if (this.inAjaxCallback) {
-            this.closePopup.bind(this).defer();
-        } else {
-            window.close();
-        }
-    },
-
-    logout: function(url)
-    {
-        this.is_logout = true;
-        this.redirect(url || (DIMP.conf.URI_AJAX + 'logOut'));
-    },
-
-    redirect: function(url, force)
-    {
-        var ptr = parent.frames.horde_main ? parent : window;
-
-        ptr.location.assign(this.addURLParam(url));
-
-        // Catch browsers that don't redirect on assign().
-        if (force && !Prototype.Browser.WebKit) {
-            (function() { ptr.location.reload(); }).delay(0.5);
-        }
-    },
-
-    loadingImg: function(elt, id, show)
-    {
-        elt = $(elt);
-
-        if (show) {
-            elt.clonePosition(id, { setHeight: false, setLeft: false, setWidth: false }).show();
-        } else {
-            elt.fade({ duration: 0.2 });
-        }
-    },
-
-    toggleButtons: function(elts, disable)
-    {
-        elts.each(function(b) {
-            var tmp;
-            [ b.up() ].invoke(disable ? 'addClassName' : 'removeClassName', 'disabled');
-            if (this.DMenu &&
-                (tmp = b.next('.popdown'))) {
-                this.DMenu.disable(tmp.identify(), true, disable);
-            }
-        }, this);
-    },
-
-    // p = (Element) Parent element
-    // t = (string) Context menu type
-    // trigger = (boolean) Trigger popdown on button click?
-    // d = (boolean) Disabled?
-    addPopdown: function(p, t, trigger, d)
-    {
-        var elt = new Element('SPAN', { className: 'iconImg popdownImg popdown' });
-        p = $(p);
-
-        p.insert({ after: elt });
-
-        if (trigger) {
-            this.addContextMenu({
-                disable: d,
-                id: p.identify(),
-                left: true,
-                offset: p.up(),
-                type: t
-            });
-        }
-
-        this.addContextMenu({
-            disable: d,
-            id: elt.identify(),
-            left: true,
-            offset: elt.up(),
-            type: t
-        });
-
-        return elt;
-    },
-
-    addContextMenu: function(p)
-    {
-        if (this.DMenu) {
-            this.DMenu.addElement(p.id, 'ctx_' + p.type, p);
-        }
-    },
-
-    /* Add dropdown menus to addresses. */
-    buildAddressLinks: function(alist, elt)
-    {
-        var base, tmp,
-            cnt = alist.size();
-
-        if (cnt > 15) {
-            tmp = $('largeaddrspan').cloneNode(true).writeAttribute('id', 'largeaddrspan_active');
-            elt.insert(tmp);
-            base = tmp.down('.dispaddrlist');
-            tmp = tmp.down('.largeaddrlist');
-            tmp.setText(tmp.getText().replace('%d', cnt));
-        } else {
-            base = elt;
-        }
-
-        alist.each(function(o, i) {
-            var a;
-            if (o.raw) {
-                a = o.raw;
-            } else {
-                a = new Element('A', { className: 'address' }).store({ personal: o.personal, email: o.inner, address: (o.personal ? (o.personal + ' <' + o.inner + '>') : o.inner) });
-                if (o.personal) {
-                    a.writeAttribute({ title: o.inner }).insert(o.personal.escapeHTML());
-                } else {
-                    a.insert(o.inner.escapeHTML());
-                }
-                this.DMenu.addElement(a.identify(), 'ctx_contacts', { offset: a, left: true });
-            }
-            base.insert(a);
-            if (i + 1 != cnt) {
-                base.insert(', ');
-            }
-        }, this);
-
-        return elt;
-    },
-
-    /* Add message log info to message view. */
-    updateMsgLog: function(log)
-    {
-        var tmp = '';
-        log.each(function(entry) {
-            tmp += '<li><span class="iconImg imp-' + entry.t + '"></span>' + entry.m + '</li>';
-        });
-        $('msgloglist').down('UL').update(tmp);
-    },
-
-    /* Removes event handlers from address links. */
-    removeAddressLinks: function(id)
-    {
-        id.select('.address').each(function(elt) {
-            this.DMenu.removeElement(elt.identify());
-        }, this);
-    },
-
-    addURLParam: function(url, params)
-    {
-        var q = url.indexOf('?');
-        params = $H(params);
-
-        if (DIMP.conf.SESSION_ID) {
-            params.update(DIMP.conf.SESSION_ID.toQueryParams());
-        }
-
-        if (q != -1) {
-            params.update(url.toQueryParams());
-            url = url.substring(0, q);
-        }
-
-        return params.size() ? (url + '?' + params.toQueryString()) : url;
-    },
-
-    reloadMessage: function(params)
-    {
-        if (typeof DimpFullmessage != 'undefined') {
-            window.location = this.addURLParam(document.location.href, params);
-        } else {
-            DimpBase.loadPreview(null, params);
-        }
-    },
-
-    /* Mouse click handler. */
-    clickHandler: function(e)
-    {
-        if (e.isRightClick()) {
-            return;
-        }
-
-        var elt = e.element(), id, tmp;
-
-        while (Object.isElement(elt)) {
-            id = elt.readAttribute('id');
-
-            switch (id) {
-            case 'largeaddrspan_active':
-                tmp = elt.down();
-                if (!tmp.next().visible() ||
-                    e.element().hasClassName('largeaddrlist')) {
-                    [ tmp.down(), tmp.down(1), tmp.next() ].invoke('toggle');
-                }
-                break;
-
-            default:
-                // CSS class based matching
-                if (elt.hasClassName('unblockImageLink')) {
-                    IMP.unblockImages(e);
-                } else if (elt.hasClassName('toggleQuoteShow')) {
-                    [ elt, elt.next() ].invoke('toggle');
-                    elt.next(1).blindDown({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } });
-                } else if (elt.hasClassName('toggleQuoteHide')) {
-                    [ elt, elt.previous() ].invoke('toggle');
-                    elt.next().blindUp({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } });
-                } else if (elt.hasClassName('pgpVerifyMsg')) {
-                    elt.replace(DIMP.text.verify);
-                    DimpCore.reloadMessage({ pgp_verify_msg: 1 });
-                    e.stop();
-                } else if (elt.hasClassName('smimeVerifyMsg')) {
-                    elt.replace(DIMP.text.verify);
-                    DimpCore.reloadMessage({ smime_verify_msg: 1 });
-                    e.stop();
-                }
-                break;
-            }
-
-            elt = elt.up();
-        }
-    },
-
-    contextOnShow: function(e)
-    {
-        var tmp;
-
-        switch (e.memo) {
-        case 'ctx_contacts':
-            tmp = $(e.memo).down('DIV.contactAddr');
-            if (tmp) {
-                tmp.next().remove();
-                tmp.remove();
-            }
-
-            // Add e-mail info to context menu if personal name is shown on
-            // page.
-            if (e.element().retrieve('personal')) {
-                $(e.memo)
-                    .insert({ top: new Element('DIV', { className: 'sep' }) })
-                    .insert({ top: new Element('DIV', { className: 'contactAddr' }).insert(e.element().retrieve('email').escapeHTML()) });
-            }
-            break;
-        }
-    },
-
-    contextOnClick: function(e)
-    {
-        var baseelt = e.element();
-
-        switch (e.memo.elt.readAttribute('id')) {
-        case 'ctx_contacts_new':
-            this.compose('new', { to: baseelt.retrieve('address') });
-            break;
-
-        case 'ctx_contacts_add':
-            this.doAction('addContact', { name: baseelt.retrieve('personal'), email: baseelt.retrieve('email') }, {}, true);
-            break;
-        }
-    },
-
-    /* DIMP initialization function. */
-    init: function()
-    {
-        if (this.is_init) {
-            return;
-        }
-        this.is_init = true;
-
-        if (typeof ContextSensitive != 'undefined') {
-            this.DMenu = new ContextSensitive();
-            document.observe('ContextSensitive:click', this.contextOnClick.bindAsEventListener(this));
-            document.observe('ContextSensitive:show', this.contextOnShow.bindAsEventListener(this));
-        }
-
-        /* Add Growler notification handler. */
-        this.Growler = new Growler({
-            location: 'br',
-            log: this.growler_log,
-            noalerts: DIMP.text.noalerts
-        });
-
-        /* Add click handler. */
-        document.observe('click', DimpCore.clickHandler.bindAsEventListener(DimpCore));
-
-        /* Catch dialog actions. */
-        document.observe('IMPDialog:success', function(e) {
-            switch (e.memo) {
-            case 'pgpPersonal':
-            case 'pgpSymmetric':
-            case 'smimePersonal':
-                IMPDialog.noreload = true;
-                this.reloadMessage({});
-                break;
-            }
-        }.bindAsEventListener(this));
-
-        /* Determine base window. Need a try/catch block here since, if the
-         * page was loaded by an opener out of this current domain, this will
-         * throw an exception. */
-        try {
-            if (parent.opener &&
-                parent.opener.location.host == window.location.host &&
-                parent.opener.DimpCore) {
-                DIMP.baseWindow = parent.opener.DIMP.baseWindow || parent.opener;
-            }
-        } catch (e) {}
-    }
-
-};
diff --git a/imp/js/ViewPort.js b/imp/js/ViewPort.js
deleted file mode 100644 (file)
index 22c43b9..0000000
+++ /dev/null
@@ -1,1846 +0,0 @@
-/**
- * ViewPort.js - Code to create a viewport window, with optional split pane
- * functionality.
- *
- * Usage:
- * ======
- * var viewport = new ViewPort({ options });
- *
- * Required options:
- * -----------------
- * ajax_url: (string) The URL to send the viewport requests to.
- *           This URL should return its response in an object named
- *           'ViewPort' (other information can be returned in the response and
- *           will be ignored by this class).
- * container: (Element/string) A DOM element/ID of the container that holds
- *            the viewport. This element should be empty and have no children.
- * onContent: (function) A function that takes 2 arguments - the data object
- *            for the row and a string indicating the current pane_mode.
- *
- *            This function MUST return the HTML representation of the row.
- *
- *            This representation MUST include both the DOM ID (stored in
- *            the VP_domid data entry) and the CSS class name (stored as an
- *            array in the VP_bg data entry) in the outermost element.
- *
- *            Selected rows will contain the classname 'vpRowSelected'.
- *
- *
- * Optional options:
- * -----------------
- * ajax_opts: (object) Any additional options to pass to the Ajax.Request
- *            object when sending an AJAX message.
- * buffer_pages: (integer) The number of viewable pages to send to the browser
- *               per server access when listing rows.
- * empty_msg: (string) A string to display when the view is empty. Inserted in
- *            a SPAN element with class 'vpEmpty'.
- * limit_factor: (integer) When browsing through a list, if a user comes
- *               within this percentage of the end of the current cached
- *               viewport, send a background request to the server to retrieve
- *               the next slice.
- * list_class: (string) The CSS class to use for the list container.
- * lookbehind: (integer) What percentage of the received buffer should be
- *             used to download rows before the given row number?
- * onAjaxFailure: (function) Callback function that handles a failure response
- *                from an AJAX request.
- *                params: (XMLHttpRequest object)
- *                        (mixed) Result of evaluating the X-JSON response
- *                        header, if any (can be null).
- *                return: NONE
- * onAjaxRequest: (function) Callback function that allows additional
- *                parameters to be added to the outgoing AJAX request.
-                  params: (string) The current view.
-                  return: (Hash) Parameters to add to the outgoing request.
- * onAjaxResponse: (function) Callback function that allows user-defined code
- *                 to additionally process the AJAX return data.
- *                params: (XMLHttpRequest object)
- *                        (mixed) Result of evaluating the X-JSON response
- *                        header, if any (can be null).
- *                return: NONE
- * onCachedList: (function) Callback function that allows the cache ID string
- *               to be dynamically generated.
-                 params: (string) The current view.
-                 return: (string) The cache ID string to use.
- * onContentOffset: (function) Callback function that alters the starting
- *                  offset of the content about to be rendered.
- *                  params: (integer) The current offset.
- *                  return: (integer) The altered offset.
- * onSlide: (function) Callback function that is triggered when the
- *          viewport slider bar is moved.
- *          params: NONE
- *          return: NONE
- * page_size: (integer) Default page size to view on load. Only used if
- *            pane_mode is 'horiz'.
- * pane_data: (Element/string) A DOM element/ID of the container to hold
- *            the split pane data. This element will be moved inside of the
- *            container element.
- * pane_mode: (string) The split pane mode to show on load? Either empty,
- *            'horiz', or 'vert'.
- * pane_width: (integer) The default pane width to use on load. Only used if
- *             pane_mode is 'vert'.
- * split_bar_class: (object) The CSS class(es) to use for the split bar.
- *                  Takes two properties: 'horiz' and 'vert'.
- * wait: (integer) How long, in seconds, to wait before displaying an
- *       informational message to users that the list is still being
- *       built.
- *
- *
- * Custom events:
- * --------------
- * Custom events are triggered on the container element. The parameters given
- * below are available through the 'memo' property of the Event object.
- *
- * ViewPort:add
- *   Fired when a row has been added to the screen.
- *   params: (Element) The viewport row being added.
- *
- * ViewPort:cacheUpdate
- *   Fired when the internal cached data of a view is changed.
- *   params: (string) View which is being updated.
- *
- * ViewPort:clear
- *   Fired when a row is being removed from the screen.
- *   params: (Element) The viewport row being removed.
- *
- * ViewPort:contentComplete
- *   Fired when the view has changed and all viewport rows have been added.
- *   params: NONE
- *
- * ViewPort:deselect
- *   Fired when rows are deselected.
- *   params: (object) opts = (object) Boolean options [right]
- *                    vs = (ViewPort_Selection) A ViewPort_Selection object.
- *
- * ViewPort:endFetch
- *   Fired when a fetch AJAX response is completed.
- *   params: (string) Current view.
- *
- * ViewPort:fetch
- *   Fired when a non-background AJAX response is sent.
- *   params: (string) Current view.
- *
- * ViewPort:select
- *   Fired when rows are selected.
- *   params: (object) opts = (object) Boolean options [delay, right]
- *                    vs = (ViewPort_Selection) A ViewPort_Selection object.
- *
- * ViewPort:splitBarChange
- *   Fired when the splitbar is moved.
- *   params: (string) The current pane mode ('horiz' or 'vert').
- *
- * ViewPort:splitBarEnd
- *   Fired when the splitbar is released.
- *   params: (string) The current pane mode ('horiz' or 'vert').
- *
- * ViewPort:splitBarStart
- *   Fired when the splitbar is initially clicked.
- *   params: (string) The current pane mode ('horiz' or 'vert').
- *
- * ViewPort:wait
- *   Fired if viewport_wait seconds have passed since request was sent.
- *   params: (string) Current view.
- *
- *
- * Outgoing AJAX request has the following params:
- * -----------------------------------------------
- * For ALL requests:
- *   cache: (string) The list of uids cached on the browser.
- *   cacheid: (string) A unique string that changes whenever the viewport
- *            list changes.
- *   initial: (integer) This is the initial browser request for this view.
- *   requestid: (integer) A unique identifier for this AJAX request.
- *   view: (string) The view of the request.
- *
- * For a row request:
- *   slice: (string) The list of rows to retrieve from the server.
- *          In the format: [first_row]:[last_row]
- *
- * For a search request:
- *   after: (integer) The number of rows to return after the selected row.
- *   before: (integer) The number of rows to return before the selected row.
- *   search: (JSON object) The search query.
- *
- * For a rangeslice request:
- *   rangeslice: (integer) If present, indicates that slice is a rangeslice
- *               request.
- *   slice: (string) The list of rows to retrieve from the server.
- *          In the format: [first_row]:[last_row]
- *
- *
- * Incoming AJAX response has the following params:
- * ------------------------------------------------
- * cacheid: (string) A unique string that changes whenever the viewport
- *          list changes.
- * data: (object) Data for each entry that is passed to the template to create
- *       the viewable rows. Keys are a unique ID (see also the 'rowlist'
- *       entry). Values are the data objects. Internal keys for these data
- *       objects must NOT begin with the string 'VP_'.
- * disappear: (array) If update is set, this is the list of unique IDs that
- *            have been cached by the browser but no longer appear on the
- *            server.
- * label: (string) [REQUIRED when initial is true] The label to use for the
- *        view.
- * metadata [optional]: (object) Metadata for the view. Entries in buffer are
- *                      updated with these entries (unless resetmd is set).
- * rangelist [optional]: (object) The list of unique IDs -> rownumbers that
- *                       correspond the the given request. Only returned for
- *                       a rangeslice request.
- * requestid: (string) The request ID sent in the outgoing AJAX request.
- * reset [optional]: (integer) If set, purges all cached data.
- * resetmd [optional]: (integer) If set, purges all user metadata.
- * rowlist: (object) A mapping of unique IDs (keys) to the row numbers
- *          (values). Row numbers start at 1.
- * rownum [optional]: (integer) The row number to position screen on.
- * totalrows: (integer) Total number of rows in the view.
- * update [optional]: (integer) If set, update the rowlist instead of
- *                    overwriting it.
- * updatecacheid [optional]: (string) If set, simply update the cacheid with
- *                           the new value. Indicates that the browser
- *                           contains the up-to-date version of the cache.
- * view: (string) The view ID of the request.
- *
- *
- * Data entries:
- * -------------
- * In addition to the data provided from the server, the following
- * dynamically created entries are also available:
- *   VP_domid: (string) The DOM ID of the row.
- *   VP_id: (string) The unique ID used to store the data entry.
- *   VP_rownum: (integer) The row number of the row.
- *
- *
- * Scroll bars use ars styled using these CSS class names:
- * -------------------------------------------------------
- * vpScroll - The scroll bar container.
- * vpScrollUp - The UP arrow.
- * vpScrollCursor - The cursor used to slide within the bounds.
- * vpScrollDown - The DOWN arrow.
- *
- *
- * Requires prototypejs 1.6+, scriptaculous 1.8+ (effects.js only), and
- * Horde's dragdrop2.js and slider2.js.
- *
- * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-/**
- * ViewPort
- */
-var ViewPort = Class.create({
-
-    initialize: function(opts)
-    {
-        this.opts = Object.extend({
-            buffer_pages: 10,
-            limit_factor: 35,
-            lookbehind: 40,
-            split_bar_class: {}
-        }, opts);
-
-        this.opts.container = $(opts.container);
-        this.opts.pane_data = $(opts.pane_data);
-
-        this.opts.content = new Element('DIV', { className: opts.list_class }).setStyle({ float: 'left', overflow: 'hidden' });
-        this.opts.container.insert(this.opts.content);
-
-        this.scroller = new ViewPort_Scroller(this);
-
-        this.split_pane = {
-            curr: null,
-            currbar: null,
-            horiz: {
-                loc: opts.page_size
-            },
-            init: false,
-            spacer: null,
-            vert: {
-                width: opts.pane_width
-            }
-        };
-        this.views = {};
-
-        this.pane_mode = opts.pane_mode;
-
-        this.isbusy = this.page_size = null;
-        this.request_num = 1;
-
-        // Init empty string now.
-        this.empty_msg = new Element('SPAN', { className: 'vpEmpty' }).insert(opts.empty_msg);
-
-        // Set up AJAX response function.
-        this.ajax_response = this.opts.onAjaxResponse || this._ajaxRequestComplete.bind(this);
-
-        Event.observe(window, 'resize', this.onResize.bind(this));
-    },
-
-    // view = (string) ID of view.
-    // opts = (object) background: (boolean) Load view in background?
-    //                 search: (object) Search parameters
-    loadView: function(view, opts)
-    {
-        var buffer, curr, ps,
-            f_opts = {},
-            init = true;
-
-        this._clearWait();
-
-        // Need a page size before we can continue - this is what determines
-        // the slice size to request from the server.
-        if (this.page_size === null) {
-            ps = this.getPageSize(this.pane_mode ? 'default' : 'max');
-            if (isNaN(ps)) {
-                return this.loadView.bind(this, view, opts).defer();
-            }
-            this.page_size = ps;
-        }
-
-        if (this.view) {
-            if (!opts.background && (view != this.view)) {
-                // Need to store current buffer to save current offset
-                buffer = this._getBuffer();
-                buffer.setMetaData({ offset: this.currentOffset() }, true);
-                this.views[this.view] = buffer;
-            }
-            init = false;
-        }
-
-        if (opts.background) {
-            f_opts = { background: true, view: view };
-        } else {
-            if (!this.view) {
-                this.onResize(true);
-            } else if (this.view != view) {
-                this.active_req = null;
-            }
-            this.view = view;
-        }
-
-        if (curr = this.views[view]) {
-            this._updateContent(curr.getMetaData('offset') || 0, f_opts);
-            if (!opts.background) {
-                this._ajaxRequest({ checkcache: 1 });
-            }
-            return;
-        }
-
-        if (!init) {
-            this.visibleRows().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
-            this.opts.content.update();
-            this.scroller.clear();
-        }
-
-        this.views[view] = buffer = this._getBuffer(view, true);
-
-        if (opts.search) {
-            f_opts.search = opts.search;
-        } else {
-            f_opts.offset = 0;
-        }
-
-        f_opts.initial = 1;
-
-        this._fetchBuffer(f_opts);
-    },
-
-    // view = ID of view
-    deleteView: function(view)
-    {
-        delete this.views[view];
-    },
-
-    // rownum = (integer) Row number
-    // opts = (Object) [noupdate, top] TODO
-    scrollTo: function(rownum, opts)
-    {
-        var s = this.scroller;
-        opts = opts || {};
-
-        s.noupdate = opts.noupdate;
-
-        switch (this.isVisible(rownum)) {
-        case -1:
-            s.moveScroll(rownum - 1);
-            break;
-
-        case 0:
-            if (opts.top) {
-                s.moveScroll(rownum - 1);
-            }
-            break;
-
-        case 1:
-            s.moveScroll(Math.min(rownum - 1, this.getMetaData('total_rows') - this.getPageSize()));
-            break;
-        }
-
-        s.noupdate = false;
-    },
-
-    // rownum = (integer) Row number
-    isVisible: function(rownum)
-    {
-        var offset = this.currentOffset();
-        return (rownum < offset + 1)
-            ? -1
-            : ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0);
-    },
-
-    // params = (object) Parameters to add to outgoing URL
-    reload: function(params)
-    {
-        this._fetchBuffer({
-            offset: this.currentOffset(),
-            params: $H(params),
-            purge: true
-        });
-    },
-
-    // vs = (Viewport_Selection) A Viewport_Selection object.
-    // opts = (object) TODO [noupdate, view]
-    remove: function(vs, opts)
-    {
-        if (!vs.size()) {
-            return;
-        }
-
-        if (this.isbusy) {
-            this.remove.bind(this, vs, opts).defer();
-            return;
-        }
-
-        this.isbusy = true;
-        opts = opts || {};
-
-        var args = { duration: 0.2, to: 0.01 },
-            visible = vs.get('div');
-
-        this.deselect(vs);
-
-        // If we have visible elements to remove, only call refresh after
-        // the last effect has finished.
-        if (visible.size()) {
-            // Set 'to' to a value slightly above 0 to prevent fade()
-            // from auto hiding.  Hiding is unnecessary, since we will be
-            // removing from the document shortly.
-            visible.slice(0, -1).invoke('fade', args);
-            args.afterFinish = this._removeids.bind(this, vs, opts);
-            visible.last().fade(args);
-        } else {
-            this._removeids(vs, opts);
-        }
-    },
-
-    // vs = (Viewport_Selection) A Viewport_Selection object.
-    // opts = (object) TODO [noupdate, view]
-    _removeids: function(vs, opts)
-    {
-        this._getBuffer(opts.view).setMetaData({ total_rows: this.getMetaData('total_rows', opts.view) - vs.size() }, true);
-
-        this._getBuffer().remove(vs.get('rownum'));
-        this.opts.container.fire('ViewPort:cacheUpdate', opts.view || this.view);
-
-        if (!opts.noupdate) {
-            this.requestContentRefresh(this.currentOffset());
-        }
-
-        this.isbusy = false;
-    },
-
-    // nowait = (boolean) If true, don't delay before resizing.
-    // size = (integer) The page size to use instead of auto-determining.
-    onResize: function(nowait, size)
-    {
-        if (!this.opts.content.visible()) {
-            return;
-        }
-
-        if (this.resizefunc) {
-            clearTimeout(this.resizefunc);
-        }
-
-        if (nowait) {
-            this._onResize(size);
-        } else {
-            this.resizefunc = this._onResize.bind(this, size).delay(0.1);
-        }
-    },
-
-    // size = (integer) The page size to use instead of auto-determining.
-    _onResize: function(size)
-    {
-        var h,
-            c = this.opts.content,
-            c_opts = {},
-            lh = this._getLineHeight(),
-            sp = this.split_pane;
-
-        if (size) {
-            this.page_size = size;
-        }
-
-        if (this.view && sp.curr != this.pane_mode) {
-            c_opts.updated = this.createSelection('div', this.visibleRows()).get('domid');
-        }
-
-        // Get split pane dimensions
-        switch (this.pane_mode) {
-        case 'horiz':
-            this._initSplitBar();
-
-            if (!size) {
-                this.page_size = (sp.horiz.loc && sp.horiz.loc > 0)
-                    ? Math.min(sp.horiz.loc, this.getPageSize('splitmax'))
-                    : this.getPageSize('default');
-            }
-            sp.horiz.loc = this.page_size;
-
-            if (sp.spacer) {
-                sp.spacer.hide();
-            }
-
-            h = lh * this.page_size;
-            c.setStyle({ height: h + 'px', width: '100%' });
-            sp.currbar.show();
-            this.opts.pane_data.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show();
-            break;
-
-        case 'vert':
-            this._initSplitBar();
-
-            if (!size) {
-                this.page_size = this.getPageSize('max');
-            }
-
-            if (!sp.vert.width) {
-                sp.vert.width = parseInt(this.opts.container.clientWidth * 0.35, 10);
-            }
-
-            if (sp.spacer) {
-                sp.spacer.hide();
-            }
-
-            h = lh * this.page_size;
-            c.setStyle({ height: h + 'px', width: sp.vert.width + 'px' });
-            sp.currbar.setStyle({ height: h + 'px' }).show();
-            this.opts.pane_data.setStyle({ height: h + 'px' }).show();
-            break;
-
-        default:
-            if (sp.curr) {
-                if (this.pane_mode == 'horiz') {
-                    sp.horiz.loc = this.page_size;
-                }
-                [ this.opts.pane_data, sp.currbar ].invoke('hide');
-                sp.curr = sp.currbar = null;
-            }
-
-            if (!size) {
-                this.page_size = this.getPageSize('max');
-            }
-
-            if (sp.spacer) {
-                sp.spacer.show();
-            } else {
-                sp.spacer = new Element('DIV').setStyle({ clear: 'left' });
-                this.opts.content.up().insert(sp.spacer);
-            }
-
-            c.setStyle({ height: (lh * this.page_size) + 'px', width: '100%' });
-            break;
-        }
-
-        if (this.view) {
-            this.requestContentRefresh(this.currentOffset(), c_opts);
-        }
-    },
-
-    // offset = (integer) TODO
-    // opts = (object) See _updateContent()
-    requestContentRefresh: function(offset, opts)
-    {
-        if (!this._updateContent(offset, opts)) {
-            return false;
-        }
-
-        var limit = this._getBuffer().isNearingLimit(offset);
-        if (limit) {
-            this._fetchBuffer({
-                background: true,
-                nearing: limit,
-                offset: offset
-            });
-        }
-
-        return true;
-    },
-
-    // opts = (object) The following parameters:
-    // One of the following is REQUIRED:
-    //   offset: (integer) Value of offset
-    //   search: (object) List of search keys/values
-    //
-    // OPTIONAL:
-    //   background: (boolean) Do fetch in background
-    //   callback: (function) A callback to run when the request is complete
-    //   initial: (boolean) Is this the initial access to this view?
-    //   nearing: (string) TODO [only used w/offset]
-    //   params: (object) Parameters to add to outgoing URL
-    //   purge: (boolean) If true, purge the current rowlist and rebuild.
-    //          Attempts to reuse the current data cache.
-    //   view: (string) The view to retrieve. Defaults to current view.
-    _fetchBuffer: function(opts)
-    {
-        if (this.isbusy) {
-            return this._fetchBuffer.bind(this, opts).defer();
-        }
-
-        this.isbusy = true;
-
-        var llist, lrows, rlist, tmp, type, value,
-            view = (opts.view || this.view),
-            b = this._getBuffer(view),
-            params = $H(opts.params),
-            r_id = this.request_num++;
-
-        // Only fire fetch event if we are loading in foreground.
-        if (!opts.background) {
-            this.opts.container.fire('ViewPort:fetch', view);
-        }
-
-        params.update({ requestid: r_id });
-
-        // Determine if we are querying via offset or a search query
-        if (opts.search || opts.initial || opts.purge) {
-            /* If this is an initial request, 'type' will be set correctly
-             * further down in the code. */
-            if (opts.search) {
-                type = 'search';
-                value = opts.search;
-                params.set('search', Object.toJSON(value));
-            }
-
-            if (opts.initial) {
-                params.set('initial', 1);
-            }
-
-            if (opts.purge) {
-                b.resetRowlist();
-            }
-
-            tmp = this._lookbehind();
-
-            params.update({
-                after: this.bufferSize() - tmp,
-                before: tmp
-            });
-        }
-
-        if (!opts.search) {
-            type = 'rownum';
-            value = opts.offset + 1;
-
-            // llist: keys - request_ids; vals - loading rownums
-            llist = b.getMetaData('llist') || $H();
-            lrows = llist.values().flatten();
-
-            b.setMetaData({ req_offset: opts.offset }, true);
-
-            /* If the current offset is part of a pending request, update
-             * the offset. */
-            if (lrows.size() &&
-                b.sliceLoaded(value, lrows)) {
-                /* One more hurdle. If we are loading in background, and now
-                 * we are in foreground, we need to search for the request
-                 * that contains the current rownum. For now, just use the
-                 * last request. */
-                if (!this.active_req && !opts.background) {
-                    this.active_req = llist.keys().numericSort().last();
-                }
-                this.isbusy = false;
-                return;
-            }
-
-            /* This gets the list of rows needed which do not already appear
-             * in the buffer. */
-            tmp = this._getSliceBounds(value, opts.nearing, view);
-            rlist = $A($R(tmp.start, tmp.end)).diff(b.getAllRows());
-
-            if (!rlist.size()) {
-                this.isbusy = false;
-                return;
-            }
-
-            /* Add rows to the loading list for the view. */
-            rlist = rlist.diff(lrows).numericSort();
-            llist.set(r_id, rlist);
-            b.setMetaData({ llist: llist }, true);
-
-            params.update({ slice: rlist.first() + ':' + rlist.last() });
-        }
-
-        if (opts.callback) {
-            tmp = b.getMetaData('callback') || $H();
-            tmp.set(r_id, opts.callback);
-            b.setMetaData({ callback: tmp }, true);
-        }
-
-        if (!opts.background) {
-            this.active_req = r_id;
-            this._handleWait();
-        }
-
-        this._ajaxRequest(params, { noslice: true, view: view });
-
-        this.isbusy = false;
-    },
-
-    // rownum = (integer) Row number
-    // nearing = (string) 'bottom', 'top', null
-    // view = (string) ID of view.
-    _getSliceBounds: function(rownum, nearing, view)
-    {
-        var b_size = this.bufferSize(),
-            ob = {}, trows;
-
-        switch (nearing) {
-        case 'bottom':
-            ob.start = rownum + this.getPageSize();
-            ob.end = ob.start + b_size;
-            break;
-
-        case 'top':
-            ob.start = Math.max(rownum - b_size, 1);
-            ob.end = rownum;
-            break;
-
-        default:
-            ob.start = rownum - this._lookbehind();
-
-            /* Adjust slice if it runs past edge of available rows. In this
-             * case, fetching a tiny buffer isn't as useful as switching
-             * the unused buffer space to the other endpoint. Always allow
-             * searching past the value of total_rows, since the size of the
-             * dataset may have increased. */
-            trows = this.getMetaData('total_rows', view);
-            if (trows) {
-                ob.end = ob.start + b_size;
-
-                if (ob.end > trows) {
-                    ob.start -= ob.end - trows;
-                }
-
-                if (ob.start < 1) {
-                    ob.end += 1 - ob.start;
-                    ob.start = 1;
-                }
-            } else {
-                ob.start = Math.max(ob.start, 1);
-                ob.end = ob.start + b_size;
-            }
-            break;
-        }
-
-        return ob;
-    },
-
-    _lookbehind: function()
-    {
-        return parseInt((this.opts.lookbehind * 0.01) * this.bufferSize(), 10);
-    },
-
-    // args = (object) The list of parameters.
-    // opts = (object) [noslice, view]
-    // Returns a Hash object
-    addRequestParams: function(args, opts)
-    {
-        opts = opts || {};
-        var cid = this.getMetaData('cacheid', opts.view),
-            cached, params, rowlist;
-
-        params = this.opts.onAjaxRequest
-            ? this.opts.onAjaxRequest(opts.view || this.view)
-            : $H();
-
-        params.update({ view: opts.view || this.view });
-
-        if (cid) {
-            params.update({ cacheid: cid });
-        }
-
-        if (!opts.noslice) {
-            rowlist = this._getSliceBounds(this.currentOffset(), null, opts.view);
-            params.update({ slice: rowlist.start + ':' + rowlist.end });
-        }
-
-        if (this.opts.onCachedList) {
-            cached = this.opts.onCachedList(opts.view || this.view);
-        } else {
-            cached = this._getBuffer(opts.view).getAllUIDs();
-            cached = cached.size()
-                ? cached.toJSON()
-                : '';
-        }
-
-        if (cached.length) {
-            params.update({ cache: cached });
-        }
-
-        return params.merge(args);
-    },
-
-    // params - (object) A list of parameters to send to server
-    // opts - (object) Args to pass to addRequestParams().
-    _ajaxRequest: function(params, other)
-    {
-        new Ajax.Request(this.opts.ajax_url, Object.extend(this.opts.ajax_opts || {}, {
-            evalJS: false,
-            evalJSON: true,
-            onComplete: this.ajax_response,
-            onFailure: this.opts.onAjaxFailure || Prototype.emptyFunction,
-            parameters: this.addRequestParams(params, other)
-        }));
-    },
-
-    _ajaxRequestComplete: function(r)
-    {
-        if (r.responseJSON) {
-            this.parseJSONResponse(r.responseJSON);
-        }
-    },
-
-    // r - (object) responseJSON returned from the server.
-    parseJSONResponse: function(r)
-    {
-        if (!r.ViewPort) {
-            return;
-        }
-
-        r = r.ViewPort;
-
-        if (r.rangelist) {
-            this.select(this.createSelection('uid', r.rangelist, r.view));
-            this.opts.container.fire('ViewPort:endFetch', r.view);
-        }
-
-        if (!Object.isUndefined(r.updatecacheid)) {
-            this._getBuffer(r.view).setMetaData({ cacheid: r.updatecacheid }, true);
-        } else if (!Object.isUndefined(r.cacheid)) {
-            this._ajaxResponse(r);
-        }
-    },
-
-    // r = (Object) viewport response object
-    _ajaxResponse: function(r)
-    {
-        if (this.isbusy) {
-            this._ajaxResponse.bind(this, r).defer();
-            return;
-        }
-
-        this.isbusy = true;
-        this._clearWait();
-
-        var callback, offset, tmp,
-            buffer = this._getBuffer(r.view),
-            llist = buffer.getMetaData('llist') || $H(),
-            updated = [];
-
-        buffer.update(Object.isArray(r.data) ? {} : r.data, Object.isArray(r.rowlist) ? {} : r.rowlist, r.metadata || {}, { reset: r.reset, resetmd: r.resetmd, update: r.update });
-
-        if (r.reset) {
-            this.select(new ViewPort_Selection());
-        } else if (r.update && r.disappear && r.disappear.size()) {
-            this.deselect(this.createSelection('uid', r.disappear, r.view));
-            buffer.removeData(r.disappear);
-        }
-
-        llist.unset(r.requestid);
-
-        tmp = {
-            cacheid: r.cacheid,
-            llist: llist,
-            total_rows: r.totalrows
-        };
-        if (r.label) {
-            tmp.label = r.label;
-        }
-        buffer.setMetaData(tmp, true);
-
-        this.opts.container.fire('ViewPort:cacheUpdate', r.view);
-
-        if (r.requestid &&
-            r.requestid == this.active_req) {
-            this.active_req = null;
-            callback = buffer.getMetaData('callback');
-            offset = buffer.getMetaData('req_offset');
-
-            if (callback && callback.get(r.requestid)) {
-                callback.get(r.requestid)(r);
-                callback.unset(r.requestid);
-            }
-
-            buffer.setMetaData({ callback: undefined, req_offset: undefined }, true);
-
-            this.opts.container.fire('ViewPort:endFetch', r.view);
-        }
-
-        if (this.view == r.view) {
-            if (r.update) {
-                updated = this.createSelection('uid', Object.keys(r.data)).get('domid');
-            }
-            this._updateContent(Object.isUndefined(r.rownum) ? (Object.isUndefined(offset) ? this.currentOffset() : offset) : Number(r.rownum) - 1, { updated: updated });
-        } else if (r.rownum) {
-            // We loaded in the background. If rownumber information was
-            // provided, we need to save this or else we will position the
-            // viewport incorrectly.
-            buffer.setMetaData({ offset: Number(r.rownum) - 1 }, true);
-        }
-
-        this.isbusy = false;
-    },
-
-    // offset = (integer) TODO
-    // opts = (object) TODO [background, updated, view]
-    _updateContent: function(offset, opts)
-    {
-        opts = opts || {};
-
-        if (!this._getBuffer(opts.view).sliceLoaded(offset)) {
-            opts.offset = offset;
-            this._fetchBuffer(opts);
-            return false;
-        }
-
-        var added = {},
-            c = this.opts.content,
-            page_size = this.getPageSize(),
-            tmp = [],
-            updated = opts.updated || [],
-            vr = this.visibleRows(),
-            fdiv, rows;
-
-        this.scroller.setSize(page_size, this.getMetaData('total_rows'));
-        this.scrollTo(offset + 1, { noupdate: true, top: true });
-
-        offset = this.currentOffset();
-        if (this.opts.onContentOffset) {
-            offset = this.opts.onContentOffset(offset);
-        }
-
-        rows = this.createSelection('rownum', $A($R(offset + 1, offset + page_size)));
-
-        if (rows.size()) {
-            fdiv = document.createDocumentFragment().appendChild(new Element('DIV'));
-
-            rows.get('dataob').each(function(r) {
-                var elt;
-                if (!updated.include(r.VP_domid) &&
-                    (elt = $(r.VP_domid))) {
-                    tmp.push(elt);
-                } else {
-                    fdiv.insert({ top: this.prepareRow(r) });
-                    added[r.VP_domid] = 1;
-                    tmp.push(fdiv.down());
-                }
-            }, this);
-
-            vr.pluck('id').diff(rows.get('domid')).each($).compact().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
-
-            c.childElements().invoke('remove');
-
-            tmp.each(function(r) {
-                c.insert(r);
-                if (added[r.identify()]) {
-                    this.opts.container.fire('ViewPort:add', r);
-                }
-            }, this);
-        } else {
-            vr.each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
-            vr.invoke('remove');
-            c.update(this.empty_msg);
-        }
-
-        this.scroller.updateDisplay();
-        this.opts.container.fire('ViewPort:contentComplete');
-
-        return true;
-    },
-
-    prepareRow: function(row)
-    {
-        var r = Object.clone(row);
-
-        r.VP_bg = this.getSelected().contains('uid', r.VP_id)
-            ? [ 'vpRowSelected' ]
-            : [];
-
-        return this.opts.onContent(r, this.pane_mode);
-    },
-
-    updateRow: function(row)
-    {
-        var d = $(row.VP_domid);
-        if (d) {
-            this.opts.container.fire('ViewPort:clear', d);
-            d.replace(this.prepareRow(row));
-            this.opts.container.fire('ViewPort:add', $(row.VP_domid));
-        }
-    },
-
-    _handleWait: function(call)
-    {
-        this._clearWait();
-
-        // Server did not respond in defined amount of time.  Alert the
-        // callback function and set the next timeout.
-        if (call) {
-            this.opts.container.fire('ViewPort:wait', this.view);
-        }
-
-        // Call wait handler every x seconds
-        if (this.opts.viewport_wait) {
-            this.waitHandler = this._handleWait.bind(this, true).delay(this.opts.viewport_wait);
-        }
-    },
-
-    _clearWait: function()
-    {
-        if (this.waitHandler) {
-            clearTimeout(this.waitHandler);
-            this.waitHandler = null;
-        }
-    },
-
-    visibleRows: function()
-    {
-        return this.opts.content.select('DIV.vpRow');
-    },
-
-    getMetaData: function(id, view)
-    {
-        return this._getBuffer(view).getMetaData(id);
-    },
-
-    setMetaData: function(vals, view)
-    {
-        this._getBuffer(view).setMetaData(vals, false);
-    },
-
-    _getBuffer: function(view, create)
-    {
-        view = view || this.view;
-
-        return (!create && this.views[view])
-            ? this.views[view]
-            : new ViewPort_Buffer(this, view);
-    },
-
-    currentOffset: function()
-    {
-        return this.scroller.currentOffset();
-    },
-
-    // return: (object) The current viewable range of the viewport.
-    //         first: Top-most row offset
-    //         last: Bottom-most row offset
-    currentViewableRange: function()
-    {
-        var offset = this.currentOffset();
-        return {
-            first: offset + 1,
-            last: Math.min(offset + this.getPageSize(), this.getMetaData('total_rows'))
-        };
-    },
-
-    _getLineHeight: function()
-    {
-        var mode = this.pane_mode || 'horiz';
-
-        if (!this.split_pane[mode].lh) {
-            // To avoid hardcoding the line height, create a temporary row to
-            // figure out what the CSS says.
-            var d = new Element('DIV', { className: this.opts.list_class }).insert(this.prepareRow({ VP_domid: null }, mode)).hide();
-            $(document.body).insert(d);
-            this.split_pane[mode].lh = d.getHeight();
-            d.remove();
-        }
-
-        return this.split_pane[mode].lh;
-    },
-
-    // (type) = (string) [null (DEFAULT), 'current', 'default', 'max']
-    // return: (integer) Number of rows in current view.
-    getPageSize: function(type)
-    {
-        switch (type) {
-        case 'current':
-            return Math.min(this.page_size, this.getMetaData('total_rows'));
-
-        case 'default':
-            return (this.pane_mode == 'vert')
-                ? this.getPageSize('max')
-                : Math.max(parseInt(this.getPageSize('max') * 0.45, 10), 5);
-
-        case 'max':
-        case 'splitmax':
-            return parseInt(this._getMaxHeight() / this._getLineHeight()) - (type == 'max' ? 0 : 1);
-
-        default:
-            return this.page_size;
-        }
-    },
-
-    _getMaxHeight: function()
-    {
-        return document.viewport.getHeight() - this.opts.content.viewportOffset()[1];
-    },
-
-    bufferSize: function()
-    {
-        // Buffer size must be at least the maximum page size.
-        return Math.round(Math.max(this.getPageSize('max') + 1, this.opts.buffer_pages * this.getPageSize()));
-    },
-
-    limitTolerance: function()
-    {
-        return Math.round(this.bufferSize() * (this.opts.limit_factor / 100));
-    },
-
-    // mode = (string) Either 'horiz', 'vert', or empty.
-    showSplitPane: function(mode)
-    {
-        this.pane_mode = mode;
-        this.onResize(true);
-    },
-
-    _initSplitBar: function()
-    {
-        var sp = this.split_pane;
-
-        if (sp.currbar) {
-            sp.currbar.hide();
-        }
-
-        sp.curr = this.pane_mode;
-
-        if (sp[this.pane_mode].bar) {
-            sp.currbar = sp[this.pane_mode].bar.show();
-            return;
-        }
-
-        sp.currbar = sp[this.pane_mode].bar = new Element('DIV', { className: this.opts.split_bar_class[this.pane_mode] });
-
-        if (!this.opts.pane_data.descendantOf(this.opts.container)) {
-            this.opts.container.insert(this.opts.pane_data.remove());
-        }
-
-        this.opts.pane_data.insert({ before: sp.currbar });
-
-        switch (this.pane_mode) {
-        case 'horiz':
-            new Drag(sp.currbar.setStyle({ clear: 'left' }), {
-                constraint: 'vertical',
-                ghosting: true,
-                nodrop: true,
-                snap: function(x, y, elt) {
-                    var sp = this.split_pane,
-                        l = parseInt((y - sp.pos) / sp.lh);
-                    if (l < 1) {
-                        l = 1;
-                    } else if (l > sp.max) {
-                        l = sp.max;
-                    }
-                    sp.lines = l;
-                    return [ x, sp.pos + (l * sp.lh) ];
-                }.bind(this)
-            });
-            break;
-
-        case 'vert':
-            new Drag(sp.currbar.setStyle({ float: 'left' }), {
-                constraint: 'horizontal',
-                ghosting: true,
-                nodrop: true,
-                snapToParent: true
-            });
-            break;
-        }
-
-        if (!sp.init) {
-            document.observe('DragDrop2:end', this._onDragEnd.bindAsEventListener(this));
-            document.observe('DragDrop2:start', this._onDragStart.bindAsEventListener(this));
-            document.observe('dblclick', this._onDragDblClick.bindAsEventListener(this));
-            sp.init = true;
-        }
-    },
-
-    _onDragStart: function(e)
-    {
-        var sp = this.split_pane;
-
-        if (e.element() != sp.currbar) {
-            return;
-        }
-
-        if (this.pane_mode == 'horiz') {
-            // Cache these values since we will be using them multiple
-            // times in snap().
-            sp.lh = this._getLineHeight();
-            sp.lines = this.page_size;
-            sp.max = this.getPageSize('splitmax');
-            sp.orig = this.page_size;
-            sp.pos = this.opts.content.positionedOffset()[1];
-        }
-
-        this.opts.container.fire('ViewPort:splitBarStart', this.pane_mode);
-    },
-
-    _onDragEnd: function(e)
-    {
-        var change, drag,
-            sp = this.split_pane;
-
-        if (e.element() != sp.currbar) {
-            return;
-        }
-
-        switch (this.pane_mode) {
-        case 'horiz':
-            this.onResize(true, sp.lines);
-            change = (sp.orig != sp.lines);
-            break;
-
-        case 'vert':
-            drag = DragDrop.Drags.getDrag(e.element());
-            sp.vert.width = drag.lastCoord[0];
-            this.opts.content.setStyle({ width: sp.vert.width + 'px' });
-            change = drag.wasDragged;
-            break;
-        }
-
-        if (change) {
-            this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode);
-        }
-        this.opts.container.fire('ViewPort:splitBarEnd', this.pane_mode);
-    },
-
-    _onDragDblClick: function(e)
-    {
-        if (e.element() != this.split_pane.currbar) {
-            return;
-        }
-
-        var change, old_size = this.page_size;
-
-        switch (this.pane_mode) {
-        case 'horiz':
-            this.onResize(true, this.getPageSize('default'));
-            change = (old_size != this.page_size);
-            break;
-
-        case 'vert':
-            this.opts.content.setStyle({ width: parseInt(this.opts.container.clientWidth * 0.45, 10) + 'px' });
-            change = true;
-        }
-
-        if (change) {
-            this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode);
-        }
-    },
-
-    getAllRows: function(view)
-    {
-        var buffer = this._getBuffer(view);
-        return buffer
-            ? buffer.getAllRows()
-            : [];
-    },
-
-    createSelection: function(format, data, view)
-    {
-        var buffer = this._getBuffer(view);
-        return buffer
-            ? new ViewPort_Selection(buffer, format, data)
-            : new ViewPort_Selection(this._getBuffer(this.view));
-    },
-
-    getSelection: function(view)
-    {
-        var buffer = this._getBuffer(view);
-        return this.createSelection('uid', buffer ? buffer.getSelected().get('uid') : [], view);
-    },
-
-    // vs = (Viewport_Selection | array) A Viewport_Selection object -or- if
-    //       opts.range is set, an array of row numbers.
-    // opts = (object) TODO [add, range, search]
-    select: function(vs, opts)
-    {
-        opts = opts || {};
-
-        var b = this._getBuffer(),
-            sel, slice;
-
-        if (opts.search) {
-            return this._fetchBuffer({
-                callback: function(r) {
-                    if (r.rownum) {
-                        this.select(this.createSelection('rownum', [ r.rownum ]), { add: opts.add, range: opts.range });
-                    }
-                }.bind(this),
-                search: opts.search
-            });
-        }
-
-        if (opts.range) {
-            slice = this.createSelection('rownum', vs);
-            if (vs.size() != slice.size()) {
-                this.opts.container.fire('ViewPort:fetch', this.view);
-                return this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() });
-            }
-            vs = slice;
-        }
-
-        if (!opts.add) {
-            sel = this.getSelected();
-            b.deselect(sel, true);
-            sel.get('div').invoke('removeClassName', 'vpRowSelected');
-        }
-        b.select(vs);
-        vs.get('div').invoke('addClassName', 'vpRowSelected');
-        this.opts.container.fire('ViewPort:select', { opts: opts, vs: vs });
-    },
-
-    // vs = (Viewport_Selection) A Viewport_Selection object.
-    // opts = (object) TODO [clearall]
-    deselect: function(vs, opts)
-    {
-        opts = opts || {};
-
-        if (vs.size() &&
-            this._getBuffer().deselect(vs, opts && opts.clearall)) {
-            vs.get('div').invoke('removeClassName', 'vpRowSelected');
-            this.opts.container.fire('ViewPort:deselect', { opts: opts, vs: vs });
-        }
-    },
-
-    getSelected: function()
-    {
-        return Object.clone(this._getBuffer().getSelected());
-    }
-
-}),
-
-/**
- * ViewPort_Scroller
- */
-ViewPort_Scroller = Class.create({
-    // Variables initialized to undefined:
-    //   noupdate, scrollDiv, scrollbar, vertscroll, vp
-
-    initialize: function(vp)
-    {
-        this.vp = vp;
-    },
-
-    _createScrollBar: function()
-    {
-        if (this.scrollDiv) {
-            return;
-        }
-
-        var c = this.vp.opts.content;
-
-        // Create the outer div.
-        this.scrollDiv = new Element('DIV', { className: 'vpScroll' }).setStyle({ float: 'left', overflow: 'hidden' }).hide();
-        c.insert({ after: this.scrollDiv });
-
-        this.scrollDiv.observe('Slider2:change', this._onScroll.bind(this));
-        if (this.vp.opts.onSlide) {
-            this.scrollDiv.observe('Slider2:slide', this.vp.opts.onSlide);
-        }
-
-        // Create scrollbar object.
-        this.scrollbar = new Slider2(this.scrollDiv, {
-            buttonclass: { up: 'vpScrollUp', down: 'vpScrollDown' },
-            cursorclass: 'vpScrollCursor',
-            pagesize: this.vp.getPageSize(),
-            totalsize: this.vp.getMetaData('total_rows')
-       });
-
-        // Mouse wheel handler.
-        c.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', function(e) {
-            var move_num = Math.min(this.vp.getPageSize(), 3);
-            this.moveScroll(this.currentOffset() + ((e.wheelDelta >= 0 || e.detail < 0) ? (-1 * move_num) : move_num));
-            /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818
-             * Need to stop or else multiple scroll events may be fired. We
-             * lose the ability to have the mousescroll bubble up, but that is
-             * more desirable than having the wrong scrolling behavior. */
-            if (Prototype.Browser.Gecko && !e.stop) {
-                Event.stop(e);
-            }
-        }.bindAsEventListener(this));
-    },
-
-    setSize: function(viewsize, totalsize)
-    {
-        this._createScrollBar();
-        this.scrollbar.setHandleLength(viewsize, totalsize);
-    },
-
-    updateDisplay: function()
-    {
-        var c = this.vp.opts.content,
-            vs = false;
-
-        if (this.scrollbar.needScroll()) {
-            switch (this.vp.pane_mode) {
-            case 'vert':
-                this.scrollDiv.setStyle({ marginLeft: 0 });
-                if (!this.vertscroll) {
-                    c.setStyle({ width: (c.clientWidth - this.scrollDiv.getWidth()) + 'px' });
-                }
-                vs = true;
-                break;
-
-            case 'horiz':
-            default:
-                this.scrollDiv.setStyle({ marginLeft: '-' + this.scrollDiv.getWidth() + 'px' });
-                break;
-            }
-
-            this.scrollDiv.setStyle({ height: c.clientHeight + 'px' });
-        } else if ((this.vp.pane_mode == 'vert') && this.vertscroll) {
-            c.setStyle({ width: (c.clientWidth + this.scrollDiv.getWidth()) + 'px' });
-        }
-
-        this.vertscroll = vs;
-        this.scrollbar.updateHandleLength();
-    },
-
-    clear: function()
-    {
-        this.setSize(0, 0);
-        this.scrollbar.updateHandleLength();
-    },
-
-    // offset = (integer) Offset to move the scrollbar to
-    moveScroll: function(offset)
-    {
-        this._createScrollBar();
-        this.scrollbar.setScrollPosition(offset);
-    },
-
-    _onScroll: function()
-    {
-        if (!this.noupdate) {
-            this.vp.requestContentRefresh(this.currentOffset());
-        }
-    },
-
-    currentOffset: function()
-    {
-        return this.scrollbar ? this.scrollbar.getValue() : 0;
-    }
-
-}),
-
-/**
- * ViewPort_Buffer
- *
- * Note: recognize the difference between offset (current location in the
- * viewport - starts at 0) with start parameters (the row numbers - starts
- * at 1).
- */
-ViewPort_Buffer = Class.create({
-
-    initialize: function(vp, view)
-    {
-        this.vp = vp;
-        this.view = view;
-        this.clear();
-    },
-
-    getView: function()
-    {
-        return this.view;
-    },
-
-    // d = (object) Data
-    // l = (object) Rowlist
-    // md = (object) User defined metadata
-    // opts = (object) TODO [reset, resetmd, update]
-    update: function(d, l, md, opts)
-    {
-        d = $H(d);
-        l = $H(l);
-        opts = opts || {};
-
-        if (!opts.reset && this.data.size()) {
-            this.data.update(d);
-        } else {
-            this.data = d;
-        }
-
-        if (opts.update || opts.reset) {
-            this.uidlist = l;
-            this.rowlist = $H();
-        } else {
-            this.uidlist = this.uidlist.size() ? this.uidlist.merge(l) : l;
-        }
-
-        l.each(function(o) {
-            this.rowlist.set(o.value, o.key);
-        }, this);
-
-        if (opts.resetmd) {
-            this.usermdata = $H(md);
-        } else {
-            $H(md).each(function(pair) {
-                if (Object.isString(pair.value) ||
-                    Object.isNumber(pair.value) ||
-                    Object.isArray(pair.value)) {
-                    this.usermdata.set(pair.key, pair.value);
-                } else {
-                    var val = this.usermdata.get(pair.key);
-                    if (val) {
-                        this.usermdata.get(pair.key).update($H(pair.value));
-                    } else {
-                        this.usermdata.set(pair.key, $H(pair.value));
-                    }
-                }
-            }, this);
-        }
-    },
-
-    // offset = (integer) Offset of the beginning of the slice.
-    // rows = (array) Additional rows to include in the search.
-    sliceLoaded: function(offset, rows)
-    {
-        var range, tr = this.getMetaData('total_rows');
-
-        // Undefined here indicates we have never sent a previous buffer
-        // request.
-        if (Object.isUndefined(tr)) {
-            return false;
-        }
-
-        range = $A($R(offset + 1, Math.min(offset + this.vp.getPageSize() - 1, tr)));
-
-        return rows
-            ? (range.diff(this.rowlist.keys().concat(rows)).size() == 0)
-            : !this._rangeCheck(range);
-    },
-
-    isNearingLimit: function(offset)
-    {
-        if (this.uidlist.size() != this.getMetaData('total_rows')) {
-            if (offset != 0 &&
-                this._rangeCheck($A($R(Math.max(offset + 1 - this.vp.limitTolerance(), 1), offset)))) {
-                return 'top';
-            } else if (this._rangeCheck($A($R(offset + 1, Math.min(offset + this.vp.limitTolerance() + this.vp.getPageSize() - 1, this.getMetaData('total_rows')))).reverse())) {
-                // Search for missing rows in reverse order since in normal
-                // usage (sequential scrolling through the row list) rows are
-                // more likely to be missing at furthest from the current
-                // view.
-                return 'bottom';
-            }
-        }
-    },
-
-    _rangeCheck: function(range)
-    {
-        return !range.all(this.rowlist.get.bind(this.rowlist));
-    },
-
-    getData: function(uids)
-    {
-        return uids.collect(function(u) {
-            var e = this.data.get(u);
-            if (!Object.isUndefined(e)) {
-                // We can directly write the rownum to the original object
-                // since we will always rewrite when creating rows.
-                e.VP_domid = 'VProw' + this.view + '_' + u;
-                e.VP_rownum = this.uidlist.get(u);
-                e.VP_id = u;
-                return e;
-            }
-        }, this).compact();
-    },
-
-    getAllUIDs: function()
-    {
-        return this.uidlist.keys();
-    },
-
-    getAllRows: function()
-    {
-        return this.rowlist.keys();
-    },
-
-    rowsToUIDs: function(rows)
-    {
-        return rows.collect(this.rowlist.get.bind(this.rowlist)).compact();
-    },
-
-    // vs = (Viewport_Selection) TODO
-    select: function(vs)
-    {
-        this.selected.add('uid', vs.get('uid'));
-    },
-
-    // vs = (Viewport_Selection) TODO
-    // clearall = (boolean) Clear all entries?
-    deselect: function(vs, clearall)
-    {
-        var size = this.selected.size();
-
-        if (clearall) {
-            this.selected.clear();
-        } else {
-            this.selected.remove('uid', vs.get('uid'));
-        }
-        return size != this.selected.size();
-    },
-
-    getSelected: function()
-    {
-        return this.selected;
-    },
-
-    // rownums = (array) Array of row numbers to remove.
-    remove: function(rownums)
-    {
-        var minrow = rownums.min(),
-            rowsize = this.rowlist.size(),
-            rowsubtract = 0,
-            newsize = rowsize - rownums.size();
-
-        return this.rowlist.keys().each(function(n) {
-            if (n >= minrow) {
-                var id = this.rowlist.get(n), r;
-                if (rownums.include(n)) {
-                    this.data.unset(id);
-                    this.uidlist.unset(id);
-                    rowsubtract++;
-                } else if (rowsubtract) {
-                    r = n - rowsubtract;
-                    this.rowlist.set(r, id);
-                    this.uidlist.set(id, r);
-                }
-                if (n > newsize) {
-                    this.rowlist.unset(n);
-                }
-            }
-        }, this);
-    },
-
-    removeData: function(uids)
-    {
-        uids.each(function(u) {
-            this.data.unset(u);
-            this.uidlist.unset(u);
-        }, this);
-    },
-
-    resetRowlist: function()
-    {
-        this.rowlist = $H();
-    },
-
-    clear: function()
-    {
-        this.data = $H();
-        this.mdata = $H({ total_rows: 0 });
-        this.rowlist = $H();
-        this.selected = new ViewPort_Selection(this);
-        this.uidlist = $H();
-        this.usermdata = $H();
-    },
-
-    getMetaData: function(id)
-    {
-        var data = this.mdata.get(id);
-
-        return Object.isUndefined(data)
-            ? this.usermdata.get(id)
-            : data;
-    },
-
-    setMetaData: function(vals, priv)
-    {
-        if (priv) {
-            this.mdata.update(vals);
-        } else {
-            this.usermdata.update(vals);
-        }
-    }
-
-}),
-
-/**
- * ViewPort_Selection
- */
-ViewPort_Selection = Class.create({
-
-    // Define property to aid in object detection
-    viewport_selection: true,
-
-    // Formats:
-    //     'dataob' = Data objects
-    //     'div' = DOM DIVs
-    //     'domid' = DOM IDs
-    //     'rownum' = Row numbers
-    //     'uid' = Unique IDs
-    initialize: function(buffer, format, data)
-    {
-        this.buffer = buffer;
-        this.clear();
-        if (!Object.isUndefined(format)) {
-            this.add(format, data);
-        }
-    },
-
-    add: function(format, d)
-    {
-        var c = this._convert(format, d);
-        this.data = this.data.size() ? this.data.concat(c).uniq() : c;
-    },
-
-    remove: function(format, d)
-    {
-        this.data = this.data.diff(this._convert(format, d));
-    },
-
-    _convert: function(format, d)
-    {
-        d = Object.isArray(d) ? d : [ d ];
-
-        // Data is stored internally as UIDs.
-        switch (format) {
-        case 'dataob':
-            return d.pluck('VP_id');
-
-        case 'div':
-            // ID here is the DOM ID of the element object.
-            d = d.pluck('id');
-            // Fall-through
-
-        case 'domid':
-            return d.invoke('substring', 6 + this.buffer.getView().length);
-
-        case 'rownum':
-            return this.buffer.rowsToUIDs(d);
-
-        case 'uid':
-            return d;
-        }
-    },
-
-    clear: function()
-    {
-        this.data = [];
-    },
-
-    get: function(format)
-    {
-        format = Object.isUndefined(format) ? 'uid' : format;
-        if (format == 'uid') {
-            return this.data;
-        }
-        var d = this.buffer.getData(this.data);
-
-        switch (format) {
-        case 'dataob':
-            return d;
-
-        case 'div':
-            return d.pluck('VP_domid').collect(function(e) { return $(e); }).compact();
-
-        case 'domid':
-            return d.pluck('VP_domid');
-
-        case 'rownum':
-            return d.pluck('VP_rownum');
-        }
-    },
-
-    contains: function(format, d)
-    {
-        return this.data.include(this._convert(format, d).first());
-    },
-
-    // params = (Object) Key is search key, value is object -> key of object
-    // must be the following:
-    //   equal - Matches any value contained in the query array.
-    //   include - Matches if this value is contained within the array.
-    //   notequal - Matches any value not contained in the query array.
-    //   notinclude - Matches if this value is not contained within the array.
-    //   regex - Matches the RegExp contained in the query.
-    search: function(params)
-    {
-        return new ViewPort_Selection(this.buffer, 'uid', this.get('dataob').findAll(function(i) {
-            // i = data object
-            return $H(params).all(function(k) {
-                // k.key = search key; k.value = search criteria
-                return $H(k.value).all(function(s) {
-                    // s.key = search type; s.value = search query
-                    switch (s.key) {
-                    case 'equal':
-                    case 'notequal':
-                        var r = i[k.key] && s.value.include(i[k.key]);
-                        return (s.key == 'equal') ? r : !r;
-
-                    case 'include':
-                    case 'notinclude':
-                        var r = i[k.key] && Object.isArray(i[k.key]) && i[k.key].include(s.value);
-                        return (s.key == 'include') ? r : !r;
-
-                    case 'regex':
-                        return i[k.key].match(s.value);
-                    }
-                });
-            });
-        }).pluck('VP_id'));
-    },
-
-    size: function()
-    {
-        return this.data.size();
-    },
-
-    set: function(vals)
-    {
-        this.get('dataob').each(function(d) {
-            $H(vals).each(function(v) {
-                d[v.key] = v.value;
-            });
-        });
-    },
-
-    getBuffer: function()
-    {
-        return this.buffer;
-    }
-
-});
-
-/** Utility Functions **/
-Object.extend(Array.prototype, {
-    // Need our own diff() function because prototypejs's without() function
-    // does not handle array input.
-    diff: function(values)
-    {
-        return this.select(function(value) {
-            return !values.include(value);
-        });
-    },
-    numericSort: function()
-    {
-        return this.collect(Number).sort(function(a, b) {
-            return (a > b) ? 1 : ((a < b) ? -1 : 0);
-        });
-    }
-});
diff --git a/imp/js/dimpbase.js b/imp/js/dimpbase.js
new file mode 100644 (file)
index 0000000..bccdd4a
--- /dev/null
@@ -0,0 +1,3143 @@
+/**
+ * dimpbase.js - Javascript used in the base DIMP page.
+ *
+ * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+var DimpBase = {
+    // Vars used and defaulting to null/false:
+    //   cfolderaction, folder, folderswitch, pollPE, pp, preview_replace,
+    //   resize, rownum, search, splitbar, template, uid, viewport
+    // msglist_template_horiz and msglist_template_vert set via
+    //   js/mailbox-dimp.js
+    cacheids: {},
+    lastrow: -1,
+    pivotrow: -1,
+    ppcache: {},
+    ppfifo: [],
+    showunsub: 0,
+    tcache: {},
+
+    // Preview pane cache size is 20 entries. Given that a reasonable guess
+    // of an average e-mail size is 10 KB (including headers), also make
+    // an estimate that the JSON data size will be approx. 10 KB. 200 KB
+    // should be a fairly safe caching value for any recent browser.
+    ppcachesize: 20,
+
+    // Message selection functions
+
+    // id = (string) DOM ID
+    // opts = (Object) Boolean options [ctrl, right, shift]
+    msgSelect: function(id, opts)
+    {
+        var bounds,
+            row = this.viewport.createSelection('domid', id),
+            rownum = row.get('rownum').first(),
+            sel = this.isSelected('domid', id),
+            selcount = this.selectedCount();
+
+        this.lastrow = rownum;
+
+        // Some browsers need to stop the mousedown event before it propogates
+        // down to the browser level in order to prevent text selection on
+        // drag/drop actions.  Clicking on a message should always lose focus
+        // from the search input, because the user may immediately start
+        // keyboard navigation after that. Thus, we need to ensure that a
+        // message click loses focus on the search input.
+        if ($('qsearch')) {
+            $('qsearch_input').blur();
+        }
+
+        if (opts.shift) {
+            if (selcount) {
+                if (!sel || selcount != 1) {
+                    bounds = [ rownum, this.pivotrow ];
+                    this.viewport.select($A($R(bounds.min(), bounds.max())), { range: true });
+                }
+                return;
+            }
+        } else if (opts.ctrl) {
+            this.pivotrow = rownum;
+            if (sel) {
+                this.viewport.deselect(row, { right: opts.right });
+                return;
+            } else if (opts.right || selcount) {
+                this.viewport.select(row, { add: true, right: opts.right });
+                return;
+            }
+        }
+
+        this.viewport.select(row, { right: opts.right });
+    },
+
+    selectAll: function()
+    {
+        this.viewport.select(this.viewport.getAllRows(), { range: true });
+    },
+
+    isSelected: function(format, data)
+    {
+        return this.viewport.getSelected().contains(format, data);
+    },
+
+    selectedCount: function()
+    {
+        return (this.viewport) ? this.viewport.getSelected().size() : 0;
+    },
+
+    resetSelected: function()
+    {
+        if (this.viewport) {
+            this.viewport.deselect(this.viewport.getSelected(), { clearall: true });
+        }
+        this.toggleButtons();
+        this.clearPreviewPane();
+    },
+
+    // num = (integer) See absolute.
+    // absolute = Is num an absolute row number - from 1 -> page_size (true) -
+    //            or a relative change from the current selected value (false)
+    //            If no current selected value, the first message in the
+    //            current viewport is selected.
+    moveSelected: function(num, absolute)
+    {
+        var curr, curr_row, row, row_data, sel;
+
+        if (absolute) {
+            if (!this.viewport.getMetaData('total_rows')) {
+                return;
+            }
+            curr = num;
+        } else {
+            if (num == 0) {
+                return;
+            }
+
+            sel = this.viewport.getSelected();
+            switch (sel.size()) {
+            case 0:
+                curr = this.viewport.currentOffset();
+                curr += (num > 0) ? 1 : this.viewport.getPageSize('current');
+                break;
+
+            case 1:
+                curr_row = sel.get('dataob').first();
+                curr = curr_row.VP_rownum + num;
+                break;
+
+            default:
+                sel = sel.get('rownum');
+                curr = (num > 0 ? sel.max() : sel.min()) + num;
+                break;
+            }
+            curr = (num > 0) ? Math.min(curr, this.viewport.getMetaData('total_rows')) : Math.max(curr, 1);
+        }
+
+        row = this.viewport.createSelection('rownum', curr);
+        if (row.size()) {
+            row_data = row.get('dataob').first();
+            if (!curr_row || row_data.imapuid != curr_row.imapuid) {
+                this.viewport.scrollTo(row_data.VP_rownum);
+                this.viewport.select(row, { delay: 0.3 });
+            }
+        } else {
+            this.rownum = curr;
+            this.viewport.requestContentRefresh(curr - 1);
+        }
+    },
+    // End message selection functions
+
+    go: function(loc, data)
+    {
+        var app, f, separator;
+
+        /* If switching from options, we need to reload page to pick up any
+         * prefs changes. */
+        if (this.folder === null &&
+            loc != 'options' &&
+            $('appoptions') &&
+            $('appoptions').hasClassName('on')) {
+            $('dimpPage').hide();
+            $('dimpLoading').show();
+            return DimpCore.redirect(DIMP.conf.URI_DIMP + '#' + loc, true);
+        }
+
+        if (loc.startsWith('compose:')) {
+            return;
+        }
+
+        if (loc.startsWith('msg:')) {
+            separator = loc.indexOf(':', 4);
+            f = loc.substring(4, separator);
+            this.uid = parseInt(loc.substring(separator + 1), 10);
+            loc = 'folder:' + f;
+            // Now fall through to the 'folder:' check below.
+        }
+
+        if (loc.startsWith('folder:')) {
+            f = loc.substring(7);
+            if (this.folder != f || !$('dimpmain_folder').visible()) {
+                this.highlightSidebar(this.getFolderId(f));
+                if (!$('dimpmain_folder').visible()) {
+                    $('dimpmain_portal').hide();
+                    $('dimpmain_folder').show();
+                }
+
+                // This catches the refresh case - no need to re-add to history
+                if (!Object.isUndefined(this.folder) && !this.search) {
+                    location.hash = encodeURIComponent(loc);
+                }
+            }
+
+            this.loadMailbox(f);
+            return;
+        }
+
+        f = this.folder;
+        this.folder = null;
+        $('dimpmain_folder').hide();
+        $('dimpmain_portal').update(DIMP.text.loading).show();
+
+        if (loc.startsWith('app:')) {
+            app = loc.substr(4);
+            if (app == 'imp') {
+                this.go('folder:INBOX');
+                return;
+            }
+            this.highlightSidebar('app' + app);
+            location.hash = encodeURIComponent(loc);
+            if (data) {
+                this.iframeContent(loc, data);
+            } else if (DIMP.conf.app_urls[app]) {
+                this.iframeContent(loc, DIMP.conf.app_urls[app]);
+            }
+            return;
+        }
+
+        switch (loc) {
+        case 'search':
+            // data: 'edit_query' = folder to edit; otherwise, loads search
+            //       screen with current mailbox as default search mailbox
+            if (!data) {
+                data = { search_mailbox: f };
+            }
+            this.highlightSidebar();
+            DimpCore.setTitle(DIMP.text.search);
+            this.iframeContent(loc, DimpCore.addURLParam(DIMP.conf.URI_SEARCH, data));
+            break;
+
+        case 'portal':
+            this.highlightSidebar('appportal');
+            location.hash = encodeURIComponent(loc);
+            DimpCore.setTitle(DIMP.text.portal);
+            DimpCore.doAction('showPortal', {}, { callback: this._portalCallback.bind(this) });
+            break;
+
+        case 'options':
+            this.highlightSidebar('appoptions');
+            location.hash = encodeURIComponent(loc);
+            DimpCore.setTitle(DIMP.text.prefs);
+            this.iframeContent(loc, DIMP.conf.URI_PREFS_IMP);
+            break;
+        }
+    },
+
+    highlightSidebar: function(id)
+    {
+        // Folder bar may not be fully loaded yet.
+        if ($('foldersLoading').visible()) {
+            this.highlightSidebar.bind(this, id).defer();
+            return;
+        }
+
+        var curr = $('sidebar').down('.on'),
+            elt = $(id);
+
+        if (curr == elt) {
+            return;
+        }
+
+        if (elt && !elt.match('LI')) {
+            elt = elt.up();
+            if (!elt) {
+                return;
+            }
+        }
+
+        if (curr) {
+            curr.removeClassName('on');
+        }
+
+        if (elt) {
+            elt.addClassName('on');
+            this._toggleSubFolder(elt, 'exp');
+        }
+    },
+
+    iframeContent: function(name, loc)
+    {
+        var container = $('dimpmain_portal'), iframe;
+        if (!container) {
+            DimpCore.showNotifications([ { type: 'horde.error', message: 'Bad portal!' } ]);
+            return;
+        }
+
+        iframe = new Element('IFRAME', { id: 'iframe' + (name === null ? loc : name), className: 'iframe', frameBorder: 0, src: loc }).setStyle({ height: document.viewport.getHeight() + 'px' });
+        container.insert(iframe);
+    },
+
+    // r = ViewPort row data
+    msgWindow: function(r)
+    {
+        this.updateSeenUID(r, 1);
+        var url = DIMP.conf.URI_MESSAGE;
+        url += (url.include('?') ? '&' : '?') +
+               $H({ folder: r.view,
+                    uid: Number(r.imapuid) }).toQueryString();
+        DimpCore.popupWindow(url, 'msgview' + r.view + r.imapuid);
+    },
+
+    composeMailbox: function(type)
+    {
+        var sel = this.viewport.getSelected();
+        if (!sel.size()) {
+            return;
+        }
+        sel.get('dataob').each(function(s) {
+            DimpCore.compose(type, { folder: s.view, uid: s.imapuid });
+        });
+    },
+
+    loadMailbox: function(f, opts)
+    {
+        var need_delete;
+        opts = opts || {};
+
+        if (!this.viewport) {
+            this._createViewPort();
+        }
+
+        if (!opts.background) {
+            this.resetSelected();
+            this.quicksearchClear(true);
+
+            if (this.folder != f) {
+                $('folderName').update(DIMP.text.loading);
+                $('msgHeader').update();
+                this.folderswitch = true;
+
+                /* Don't cache results of search folders - since we will need
+                 * to grab new copy if we ever return to it. */
+                if (this.isSearch(this.folder)) {
+                    need_delete = this.folder;
+                }
+
+                this.folder = f;
+
+                if (this.isSearch(f)) {
+                    if (!this.search || this.search.flag) {
+                        this._quicksearchDeactivate(!this.search);
+                    }
+                    $('refreshlink').show();
+                } else {
+                    $('refreshlink').hide();
+                }
+            }
+        }
+
+        this.viewport.loadView(f, { search: (this.uid ? { imapuid: Number(this.uid) } : null), background: opts.background});
+
+        if (need_delete) {
+            this.viewport.deleteView(need_delete);
+        }
+    },
+
+    _createViewPort: function()
+    {
+        var container = $('msgSplitPane');
+
+        [ $('msglistHeader') ].invoke(DIMP.conf.preview_pref == 'vert' ? 'hide' : 'show');
+
+        this.template = {
+            horiz: new Template(this.msglist_template_horiz),
+            vert: new Template(this.msglist_template_vert)
+        };
+
+        this.viewport = new ViewPort({
+            // Mandatory config
+            ajax_url: DIMP.conf.URI_AJAX + 'viewPort',
+            container: container,
+            onContent: function(r, mode) {
+                var bg, re, u,
+                    thread = $H(this.viewport.getMetaData('thread')),
+                    tsort = (this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v);
+
+                r.subjectdata = r.status = '';
+                r.subjecttitle = r.subject;
+
+                // Add thread graphics
+                if (tsort) {
+                    u = thread.get(r.imapuid);
+                    if (u) {
+                        $R(0, u.length, true).each(function(i) {
+                            var c = u.charAt(i);
+                            if (!this.tcache[c]) {
+                                this.tcache[c] = '<span class="treeImg treeImg' + c + '"></span>';
+                            }
+                            r.subjectdata += this.tcache[c];
+                        }, this);
+                    }
+                }
+
+                /* Generate the status flags. */
+                if (r.flag) {
+                    r.flag.each(function(a) {
+                        var ptr = DIMP.conf.flags[a];
+                        if (ptr.p) {
+                            if (!ptr.elt) {
+                                /* Until text-overflow is supported on all
+                                 * browsers, need to truncate label text
+                                 * ourselves. */
+                                ptr.elt = '<span class="' + ptr.c + '" title="' + ptr.l + '" style="background:' + ptr.b + ';color:' + ptr.f + '">' + ptr.l.truncate(10) + '</span>';
+                            }
+                            r.subjectdata += ptr.elt;
+                        } else {
+                            if (!ptr.elt) {
+                                ptr.elt = '<div class="msgflags ' + ptr.c + '" title="' + ptr.l + '"></div>';
+                            }
+                            r.status += ptr.elt;
+
+                            r.VP_bg.push(ptr.c);
+
+                            if (ptr.b) {
+                                bg = ptr.b;
+                            }
+                        }
+                    });
+                }
+
+                // Set bg
+                if (bg) {
+                    r.style = 'background:' + bg;
+                }
+
+                // Check for search strings
+                if (this.isSearch(null, true)) {
+                    re = new RegExp("(" + $F('qsearch_input') + ")", "i");
+                    [ 'from', 'subject' ].each(function(h) {
+                        r[h] = r[h].gsub(re, '<span class="qsearchMatch">#{1}</span>');
+                    });
+                }
+
+                // If these fields are null, invalid string was scrubbed by
+                // JSON encode.
+                if (r.from === null) {
+                    r.from = '[' + DIMP.text.badaddr + ']';
+                }
+                if (r.subject === null) {
+                    r.subject = r.subjecttitle = '[' + DIMP.text.badsubject + ']';
+                }
+
+                r.VP_bg.push('vpRow');
+
+                switch (mode) {
+                case 'vert':
+                    r.VP_bg.unshift('vpRowVert');
+                    r.className = r.VP_bg.join(' ');
+                    return this.template.vert.evaluate(r);
+
+                default:
+                    r.VP_bg.unshift('vpRowHoriz');
+                    r.className = r.VP_bg.join(' ');
+                    return this.template.horiz.evaluate(r);
+                }
+            }.bind(this),
+
+            // Optional config
+            ajax_opts: Object.clone(DimpCore.doActionOpts),
+            buffer_pages: DIMP.conf.buffer_pages,
+            empty_msg: DIMP.text.vp_empty,
+            list_class: 'msglist',
+            page_size: DIMP.conf.splitbar_pos,
+            pane_data: 'previewPane',
+            pane_mode: DIMP.conf.preview_pref,
+            split_bar_class: { horiz: 'splitBarHoriz', vert: 'splitBarVert' },
+            wait: DIMP.conf.viewport_wait,
+
+            // Callbacks
+            onAjaxFailure: function() {
+                if ($('dimpmain_folder').visible()) {
+                    DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.listmsg_timeout } ]);
+                }
+                this.loadingImg('viewport', false);
+            }.bind(this),
+            onAjaxRequest: function(id) {
+                var p = $H();
+                if (this.folderswitch && this.isSearch(id, true)) {
+                    p.set('qsearchmbox', this.search.mbox);
+                    if (this.search.flag) {
+                        p.update({ qsearchflag: this.search.flag, qsearchflagnot: Number(this.convertFlag(this.search.flag, this.search.not)) });
+                    } else {
+                        p.set('qsearch', $F('qsearch_input'));
+                    }
+                }
+                return DimpCore.addRequestParams(p);
+            }.bind(this),
+            onAjaxResponse: function(o, h) {
+                DimpCore.doActionComplete(o);
+            },
+            onCachedList: function(id) {
+                if (!this.cacheids[id]) {
+                    var vs = this.viewport.getSelection(id);
+                    if (!vs.size()) {
+                        return '';
+                    }
+
+                    this.cacheids[id] = DimpCore.toRangeString(DimpCore.selectionToRange(vs));
+                }
+                return this.cacheids[id];
+            }.bind(this),
+            onContentOffset: function(offset) {
+                if (this.uid) {
+                    var row = this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ this.uid ] }, view: { equal: [ this.folder ] } });
+                    if (row.size()) {
+                        this.rownum = row.get('rownum').first();
+                    }
+                    this.uid = null;
+                }
+
+                if (this.rownum) {
+                    this.viewport.scrollTo(this.rownum, { noupdate: true, top: true });
+                    offset = this.viewport.currentOffset();
+                }
+
+                return offset;
+            }.bind(this),
+            onSlide: this.setMessageListTitle.bind(this)
+        });
+
+        /* Custom ViewPort events. */
+        container.observe('ViewPort:add', function(e) {
+            var row = e.memo.identify();
+            DimpCore.addContextMenu({
+                id: row,
+                type: 'message'
+            });
+            new Drag(row, this._msgDragConfig);
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:cacheUpdate', function(e) {
+            delete this.cacheids[e.memo];
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:clear', function(e) {
+            this._removeMouseEvents(e.memo);
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:contentComplete', function() {
+            var flags, ssc, tmp,
+                ham = spam = 'show',
+                l = this.viewport.getMetaData('label');
+
+            this.setMessageListTitle();
+            if (!this.isSearch()) {
+                this.setFolderLabel(this.folder, this.viewport.getMetaData('unseen') || 0);
+            }
+            this.updateTitle();
+
+            if (this.rownum) {
+                this.viewport.select(this.viewport.createSelection('rownum', this.rownum));
+                this.rownum = null;
+            }
+
+            // 'label' will not be set if there has been an error
+            // retrieving data from the server.
+            l = this.viewport.getMetaData('label');
+            if (l) {
+                if (this.isSearch(null, true)) {
+                    l += ' (' + this.search.label + ')';
+                }
+                $('folderName').update(l);
+            }
+
+            if (this.folderswitch) {
+                this.folderswitch = false;
+
+                tmp = $('applyfilterlink');
+                if (tmp) {
+                    if (this.isSearch() ||
+                        (!DIMP.conf.filter_any &&
+                         this.folder.toUpperCase() != 'INBOX')) {
+                        tmp.hide();
+                    } else {
+                        tmp.show();
+                    }
+                }
+
+                if (this.folder == DIMP.conf.spam_mbox) {
+                    if (!DIMP.conf.spam_spammbox) {
+                        spam = 'hide';
+                    }
+                } else if (DIMP.conf.ham_spammbox) {
+                    ham = 'hide';
+                }
+
+                if ($('button_ham')) {
+                    [ $('button_ham').up(), $('ctx_message_ham') ].invoke(ham);
+                }
+                if ($('button_spam')) {
+                    [ $('button_spam').up(), $('ctx_message_spam') ].invoke(spam);
+                }
+
+                /* Read-only changes. 'oa_setflag' is handled elsewhere. */
+                tmp = [ $('button_deleted') ].compact().invoke('up', 'SPAN').concat($('ctx_message_deleted', 'ctx_message_setflag', 'ctx_message_undeleted'));
+
+                if (this.viewport.getMetaData('readonly')) {
+                    tmp.compact().invoke('hide');
+                    $('folderName').next().show();
+                } else {
+                    tmp.compact().invoke('show');
+                    $('folderName').next().hide();
+                }
+            } else if (this.filtertoggle &&
+                       this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v) {
+                ssc = $H(DIMP.conf.sort).get('date').v;
+            }
+
+            this.setSortColumns(ssc);
+
+            /* Context menu: generate the list of settable flags for this
+             * mailbox. */
+            flags = this.viewport.getMetaData('flags');
+            $('ctx_message_setflag', 'oa_setflag').invoke('up').invoke(flags.size() ? 'show' : 'hide');
+            if (flags.size()) {
+                $('ctx_flag').childElements().each(function(c) {
+                    [ c ].invoke(flags.include(c.readAttribute('flag')) ? 'show' : 'hide');
+                });
+            }
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:deselect', function(e) {
+            var sel = this.viewport.getSelected(),
+                count = sel.size();
+            if (!count) {
+                this.lastrow = this.pivotrow = -1;
+            }
+
+            this.toggleButtons();
+            if (e.memo.opts.right || !count) {
+                if (!this.preview_replace) {
+                    this.clearPreviewPane();
+                }
+            } else if ((count == 1) && DIMP.conf.preview_pref) {
+                this.loadPreview(sel.get('dataob').first());
+            }
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:endFetch', this.loadingImg.bind(this, 'viewport', false));
+
+        container.observe('ViewPort:fetch', this.loadingImg.bind(this, 'viewport', true));
+
+        container.observe('ViewPort:select', function(e) {
+            var d = e.memo.vs.get('rownum');
+            if (d.size() == 1) {
+                this.lastrow = this.pivotrow = d.first();
+            }
+
+            this.toggleButtons();
+
+            if (DIMP.conf.preview_pref) {
+                if (e.memo.opts.right) {
+                    this.clearPreviewPane();
+                } else {
+                    if (e.memo.opts.delay) {
+                        this.initPreviewPane.bind(this).delay(e.memo.opts.delay);
+                    } else {
+                        this.initPreviewPane();
+                    }
+                }
+            }
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:splitBarChange', function(e) {
+            if (e.memo = 'horiz') {
+                this._updatePrefs('dimp_splitbar', this.viewport.getPageSize());
+            }
+        }.bindAsEventListener(this));
+
+        container.observe('ViewPort:wait', function() {
+            if ($('dimpmain_folder').visible()) {
+                DimpCore.showNotifications([ { type: 'horde.warning', message: DIMP.text.listmsg_wait } ]);
+            }
+        });
+    },
+
+    _removeMouseEvents: function(elt)
+    {
+        var d, id = $(elt).readAttribute('id');
+
+        if (id) {
+            if (d = DragDrop.Drags.getDrag(id)) {
+                d.destroy();
+            }
+
+            DimpCore.DMenu.removeElement(id);
+        }
+    },
+
+    contextOnClick: function(parentfunc, e)
+    {
+        var flag, tmp,
+            baseelt = e.element(),
+            elt = e.memo.elt,
+            id = elt.readAttribute('id'),
+            menu = e.memo.trigger;
+
+        switch (id) {
+        case 'ctx_folder_create':
+            this.createSubFolder(baseelt);
+            break;
+
+        case 'ctx_container_rename':
+        case 'ctx_folder_rename':
+            this.renameFolder(baseelt);
+            break;
+
+        case 'ctx_folder_empty':
+            tmp = baseelt.up('LI');
+            if (window.confirm(DIMP.text.empty_folder.sub('%s', tmp.readAttribute('title')))) {
+                DimpCore.doAction('emptyMailbox', { mbox: tmp.retrieve('mbox') }, { callback: this._emptyMailboxCallback.bind(this) });
+            }
+            break;
+
+        case 'ctx_folder_delete':
+        case 'ctx_vfolder_delete':
+            tmp = baseelt.up('LI');
+            if (window.confirm(DIMP.text.delete_folder.sub('%s', tmp.readAttribute('title')))) {
+                DimpCore.doAction('deleteMailbox', { mbox: tmp.retrieve('mbox') }, { callback: this.mailboxCallback.bind(this) });
+            }
+            break;
+
+        case 'ctx_folder_seen':
+        case 'ctx_folder_unseen':
+            this.flagAll('\\seen', id == 'ctx_folder_seen', baseelt.up('LI').retrieve('mbox'));
+            break;
+
+        case 'ctx_folder_poll':
+        case 'ctx_folder_nopoll':
+            this.modifyPoll(baseelt.up('LI').retrieve('mbox'), id == 'ctx_folder_poll');
+            break;
+
+        case 'ctx_folder_sub':
+        case 'ctx_folder_unsub':
+            this.subscribeFolder(baseelt.up('LI').retrieve('mbox'), id == 'ctx_folder_sub');
+            break;
+
+        case 'ctx_container_create':
+            this.createSubFolder(baseelt);
+            break;
+
+        case 'ctx_folderopts_new':
+            this.createBaseFolder();
+            break;
+
+        case 'ctx_folderopts_sub':
+        case 'ctx_folderopts_unsub':
+            this.toggleSubscribed();
+            break;
+
+        case 'ctx_folderopts_expand':
+        case 'ctx_folderopts_collapse':
+            this._toggleSubFolder($('normalfolders'), id == 'ctx_folderopts_expand' ? 'expall' : 'colall', true);
+            break;
+
+        case 'ctx_folderopts_reload':
+            this._reloadFolders();
+            break;
+
+        case 'ctx_container_expand':
+        case 'ctx_container_collapse':
+        case 'ctx_folder_expand':
+        case 'ctx_folder_collapse':
+            this._toggleSubFolder(baseelt.up('LI').next(), (id == 'ctx_container_expand' || id == 'ctx_folder_expand') ? 'expall' : 'colall', true);
+            break;
+
+        case 'ctx_message_spam':
+        case 'ctx_message_ham':
+            this.reportSpam(id == 'ctx_message_spam');
+            break;
+
+        case 'ctx_message_blacklist':
+        case 'ctx_message_whitelist':
+            this.blacklist(id == 'ctx_message_blacklist');
+            break;
+
+        case 'ctx_message_deleted':
+            this.deleteMsg();
+            break;
+
+        case 'ctx_message_forward':
+        case 'ctx_message_reply':
+            this.composeMailbox(id == 'ctx_message_forward' ? 'forward_auto' : 'reply_auto');
+            break;
+
+        case 'ctx_message_source':
+            this.viewport.getSelected().get('dataob').each(function(v) {
+                DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: v.imapuid, mailbox: v.view, actionID: 'view_source', id: 0 }, true), v.imapuid + '|' + v.view);
+            }, this);
+            break;
+
+        case 'ctx_message_resume':
+            this.composeMailbox('resume');
+            break;
+
+        case 'ctx_reply_reply':
+        case 'ctx_reply_reply_all':
+        case 'ctx_reply_reply_list':
+            this.composeMailbox(id.substring(10));
+            break;
+
+        case 'ctx_forward_attach':
+        case 'ctx_forward_body':
+        case 'ctx_forward_both':
+        case 'ctx_forward_redirect':
+            this.composeMailbox(id.substring(4));
+            break;
+
+        case 'oa_preview_hide':
+            DIMP.conf.preview_pref_old = DIMP.conf.preview_pref;
+            this.togglePreviewPane('');
+            break;
+
+        case 'oa_preview_show':
+            this.togglePreviewPane(DIMP.conf.preview_pref_old || 'horiz');
+            break;
+
+        case 'oa_layout_horiz':
+        case 'oa_layout_vert':
+            this.togglePreviewPane(id.substring(10));
+            break;
+
+        case 'oa_blacklist':
+        case 'oa_whitelist':
+            this.blacklist(id == 'oa_blacklist');
+            break;
+
+        case 'ctx_message_undeleted':
+        case 'oa_undeleted':
+            this.flag('\\deleted', false);
+            break;
+
+        case 'oa_selectall':
+            this.selectAll();
+            break;
+
+        case 'oa_purge_deleted':
+            this.purgeDeleted();
+            break;
+
+        case 'ctx_vfolder_edit':
+            tmp = { edit_query: baseelt.up('LI').retrieve('mbox') };
+            // Fall through
+
+        case 'ctx_qsearchopts_advanced':
+            this.go('search', tmp);
+            break;
+
+        case 'ctx_qsearchby_all':
+        case 'ctx_qsearchby_body':
+        case 'ctx_qsearchby_from':
+        case 'ctx_qsearchby_to':
+        case 'ctx_qsearchby_subject':
+            DIMP.conf.qsearchfield = id.substring(14);
+            this._updatePrefs('dimp_qsearch_field', DIMP.conf.qsearchfield);
+            if (!$('qsearch').hasClassName('qsearchActive')) {
+                this._setQsearchText(true);
+            }
+            break;
+
+        case 'ctx_mboxsort_none':
+            this.sort($H(DIMP.conf.sort).get('sequence').v);
+            break;
+
+        default:
+            if (menu.endsWith('_setflag') || menu.endsWith('_unsetflag')) {
+                flag = elt.readAttribute('flag');
+                this.flag(flag, this.convertFlag(flag, menu.endsWith('_setflag')));
+            } else if (menu.endsWith('_filter') || menu.endsWith('_filternot')) {
+                this.search = {
+                    flag: elt.readAttribute('flag'),
+                    label: this.viewport.getMetaData('label'),
+                    mbox: this.folder,
+                    not: menu.endsWith('_filternot')
+                };
+                this.loadMailbox(DIMP.conf.fsearchid);
+            } else {
+                parentfunc(e);
+            }
+            break;
+        }
+    },
+
+    contextOnShow: function(parentfunc, e)
+    {
+        var elts, ob, sel, tmp,
+            baseelt = e.element(),
+            ctx_id = e.memo;
+
+        switch (ctx_id) {
+        case 'ctx_folder':
+            elts = $('ctx_folder_create', 'ctx_folder_rename', 'ctx_folder_delete');
+            baseelt = baseelt.up('LI');
+
+            if (baseelt.retrieve('mbox') == 'INBOX') {
+                elts.invoke('hide');
+                if ($('ctx_folder_sub')) {
+                    $('ctx_folder_sub', 'ctx_folder_unsub').invoke('hide');
+                }
+            } else {
+                if ($('ctx_folder_sub')) {
+                    tmp = baseelt.hasClassName('unsubFolder');
+                    [ $('ctx_folder_sub') ].invoke(tmp ? 'show' : 'hide');
+                    [ $('ctx_folder_unsub') ].invoke(tmp ? 'hide' : 'show');
+                }
+
+                if (DIMP.conf.fixed_folders &&
+                    DIMP.conf.fixed_folders.indexOf(baseelt.retrieve('mbox')) != -1) {
+                    elts.shift();
+                    elts.invoke('hide');
+                } else {
+                    elts.invoke('show');
+                }
+            }
+
+            tmp = Object.isUndefined(baseelt.retrieve('u'));
+            [ $('ctx_folder_poll') ].invoke(tmp ? 'show' : 'hide');
+            [ $('ctx_folder_nopoll') ].invoke(tmp ? 'hide' : 'show');
+
+            tmp = $(this.getSubFolderId(baseelt.readAttribute('id')));
+            [ $('ctx_folder_expand').up() ].invoke(tmp ? 'show' : 'hide');
+            break;
+
+        case 'ctx_reply':
+            sel = this.viewport.getSelected();
+            if (sel.size() == 1) {
+                ob = sel.get('dataob').first();
+            }
+            [ $('ctx_reply_reply_list') ].invoke(ob && ob.listmsg ? 'show' : 'hide');
+            break;
+
+        case 'ctx_otheractions':
+            switch (DIMP.conf.preview_pref) {
+            case 'vert':
+                $('oa_preview_hide', 'oa_layout_horiz').invoke('show');
+                $('oa_preview_show', 'oa_layout_vert').invoke('hide');
+                break;
+
+            case 'horiz':
+                $('oa_preview_hide', 'oa_layout_vert').invoke('show');
+                $('oa_preview_show', 'oa_layout_horiz').invoke('hide');
+                break;
+
+            default:
+                $('oa_preview_hide', 'oa_layout_horiz', 'oa_layout_vert').invoke('hide');
+                $('oa_preview_show').show();
+                break;
+            }
+            tmp = [ $('oa_undeleted') ];
+            $('oa_blacklist', 'oa_whitelist').each(function(o) {
+                if (o) {
+                    tmp.push(o.up());
+                }
+            });
+            if ($('oa_setflag')) {
+                if (this.viewport.getMetaData('readonly')) {
+                    $('oa_setflag').up().hide();
+                } else {
+                    tmp.push($('oa_setflag').up());
+                }
+            }
+            tmp.compact().invoke(this.viewport.getSelected().size() ? 'show' : 'hide');
+            break;
+
+        case 'ctx_qsearchby':
+            $(ctx_id).descendants().invoke('removeClassName', 'contextSelected');
+            $(ctx_id + '_' + DIMP.conf.qsearchfield).addClassName('contextSelected');
+            break;
+
+        case 'ctx_message':
+            [ $('ctx_message_source').up() ].invoke(DIMP.conf.preview_pref ? 'hide' : 'show');
+            sel = this.viewport.getSelected();
+            [ $('ctx_message_resume') ].invoke(sel.size() == 1 && sel.get('dataob').first().draft ? 'show' : 'hide');
+            break;
+
+        default:
+            parentfunc(e);
+            break;
+        }
+    },
+
+    updateTitle: function()
+    {
+        var elt, unseen,
+            label = this.viewport.getMetaData('label');
+
+        if (this.isSearch(null, true)) {
+            label += ' (' + this.search.label + ')';
+        } else {
+            elt = $(this.getFolderId(this.folder));
+            if (elt) {
+                unseen = elt.retrieve('u');
+                if (unseen > 0) {
+                    label += ' (' + unseen + ')';
+                }
+            } else {
+                this.updateTitle.bind(this).defer();
+            }
+        }
+        DimpCore.setTitle(label);
+    },
+
+    sort: function(sortby)
+    {
+        var s;
+
+        if (Object.isUndefined(sortby)) {
+            return;
+        }
+
+        sortby = Number(sortby);
+        if (sortby == this.viewport.getMetaData('sortby')) {
+            s = { sortdir: (this.viewport.getMetaData('sortdir') ? 0 : 1) };
+            this.viewport.setMetaData({ sortdir: s.sortdir });
+        } else {
+            s = { sortby: sortby };
+            this.viewport.setMetaData({ sortby: s.sortby });
+        }
+
+        this.setSortColumns(sortby);
+        this.viewport.reload(s);
+    },
+
+    setSortColumns: function(sortby)
+    {
+        var hdr, tmp,
+            ptr = DIMP.conf.sort,
+            m = $('msglistHeader');
+
+        if (Object.isUndefined(sortby)) {
+            sortby = this.viewport.getMetaData('sortby');
+        }
+
+        /* Init once per load. */
+        if (Object.isHash(ptr)) {
+            m.childElements().invoke('removeClassName', 'sortup').invoke('removeClassName', 'sortdown');
+        } else {
+            DIMP.conf.sort = ptr = $H(ptr);
+            ptr.each(function(s) {
+                s.value.e = new Element('A', { className: 'widget' }).store('sortby', s.value.v).insert(s.value.t);
+            }, this);
+
+            m.down('.msgFrom').update(ptr.get('from').e).insert(ptr.get('to').e);
+            m.down('.msgSize').update(ptr.get('size').e);
+            m.down('.msgDate').update(ptr.get('date').e);
+        }
+
+        /* Toggle between From/To header. */
+        tmp = m.down('.msgFrom a');
+        if (this.viewport.getMetaData('special')) {
+            tmp.hide().next().show();
+        } else {
+            tmp.show().next().hide();
+        }
+
+        /* Toggle between Subject/Thread header. */
+        tmp = m.down('.msgSubject');
+        if (this.isSearch() ||
+            this.viewport.getMetaData('nothread')) {
+            hdr = { l: 'subject', t: tmp };
+        } else if (sortby == ptr.get('thread').v) {
+            hdr = { l: 'thread', s: 'subject', t: tmp };
+        } else {
+            hdr = { l: 'subject', s: 'thread', t: tmp };
+        }
+
+        hdr.t.update().update(ptr.get(hdr.l).e.removeClassName('smallSort').update(ptr.get(hdr.l).t));
+        if (hdr.s) {
+            hdr.t.insert(ptr.get(hdr.s).e.addClassName('smallSort').update('[' + ptr.get(hdr.s).t + ']'));
+        }
+
+        ptr.find(function(s) {
+            if (sortby != s.value.v) {
+                return false;
+            }
+            var elt = s.value.e.up();
+            if (elt) {
+                elt.addClassName(this.viewport.getMetaData('sortdir') ? 'sortup' : 'sortdown');
+            }
+            return true;
+        }, this);
+    },
+
+    // Preview pane functions
+    // mode = (string) Either 'horiz', 'vert', or empty
+    togglePreviewPane: function(mode)
+    {
+        var old = DIMP.conf.preview_pref;
+        if (mode != DIMP.conf.preview_pref) {
+            DIMP.conf.preview_pref = mode;
+            this._updatePrefs('dimp_show_preview', mode);
+            [ $('msglistHeader') ].invoke(mode == 'vert' ? 'hide' : 'show');
+            this.viewport.showSplitPane(mode);
+            if (!old) {
+                this.initPreviewPane();
+            }
+        }
+    },
+
+    loadPreview: function(data, params)
+    {
+        var pp_uid;
+
+        if (!DIMP.conf.preview_pref) {
+            return;
+        }
+
+        if (!params) {
+            if (this.pp &&
+                this.pp.imapuid == data.imapuid &&
+                this.pp.view == data.view) {
+                return;
+            }
+            this.pp = data;
+            pp_uid = this._getPPId(data.imapuid, data.view);
+
+            if (this.ppfifo.indexOf(pp_uid) != -1) {
+                  // There is a chance that the message may have been marked
+                  // as unseen since first being viewed. If so, we need to
+                  // explicitly flag as seen here. TODO?
+                if (!this.hasFlag('\\seen', data)) {
+                    this.flag('\\seen', true);
+                }
+                return this._loadPreviewCallback(this.ppcache[pp_uid]);
+            }
+        }
+
+        this.loadingImg('msg', true);
+
+        DimpCore.doAction('showPreview', this.viewport.addRequestParams(params || {}), { uids: this.viewport.createSelection('dataob', this.pp), callback: this._loadPreviewCallback.bind(this) });
+    },
+
+    _loadPreviewCallback: function(resp)
+    {
+        var bg, ppuid, row, search, tmp,
+            pm = $('previewMsg'),
+            r = resp.response.preview,
+            t = $('msgHeadersContent').down('THEAD');
+
+        bg = (this.pp &&
+              (this.pp.imapuid != r.uid || this.pp.view != r.mailbox));
+
+        if (!r.error) {
+            search = this.viewport.getSelection().search({ imapuid: { equal: [ r.uid ] }, view: { equal: [ r.mailbox ] } });
+            if (search.size()) {
+                row = search.get('dataob').first();
+                this.updateSeenUID(row, 1);
+            }
+        }
+
+        if (r.error || this.viewport.getSelected().size() != 1) {
+            if (!bg) {
+                if (r.error) {
+                    DimpCore.showNotifications([ { type: r.errortype, message: r.error } ]);
+                }
+                this.clearPreviewPane();
+            }
+            return;
+        }
+
+        // Store in cache.
+        ppuid = this._getPPId(r.uid, r.mailbox);
+        this._expirePPCache([ ppuid ]);
+        this.ppcache[ppuid] = resp;
+        this.ppfifo.push(ppuid);
+
+        if (bg) {
+            return;
+        }
+
+        DimpCore.removeAddressLinks(pm);
+
+        // Add subject
+        tmp = pm.select('.subject');
+        tmp.invoke('update', r.subject === null ? '[' + DIMP.text.badsubject + ']' : r.subject);
+
+        // Add date
+        [ $('msgHeadersColl').select('.date'), $('msgHeaderDate').select('.date') ].flatten().invoke('update', r.localdate);
+
+        // Add from/to/cc headers
+        [ 'from', 'to', 'cc' ].each(function(a) {
+            if (r[a]) {
+                (a == 'from' ? pm.select('.' + a) : [ t.down('.' + a) ]).each(function(elt) {
+                    elt.replace(DimpCore.buildAddressLinks(r[a], elt.cloneNode(false)));
+                });
+            }
+            [ $('msgHeader' + a.capitalize()) ].invoke(r[a] ? 'show' : 'hide');
+        });
+
+        // Add attachment information
+        if (r.atc_label) {
+            $('msgAtc').show();
+            tmp = $('partlist');
+            tmp.hide().previous().update(new Element('SPAN', { className: 'atcLabel' }).insert(r.atc_label)).insert(r.atc_download);
+            if (r.atc_list) {
+                $('partlist_col').show();
+                $('partlist_exp').hide();
+                tmp.down('TABLE').update(r.atc_list);
+            }
+        } else {
+            $('msgAtc').hide();
+        }
+
+        // Add message information
+        if (r.log) {
+            this.updateMsgLog(r.log);
+        } else {
+            $('msgLogInfo').hide();
+        }
+
+        $('messageBody').update(r.msgtext);
+        this.loadingImg('msg', false);
+        $('previewInfo').hide();
+        $('previewPane').scrollTop = 0;
+        pm.show();
+
+        if (r.js) {
+            eval(r.js.join(';'));
+        }
+
+        location.hash = encodeURIComponent('msg:' + row.view + ':' + row.imapuid);
+    },
+
+    _stripAttachmentCallback: function(r)
+    {
+        // Let the normal viewport refresh code and preview display code
+        // handle replacing the current preview. Set preview_replace to
+        // prevent a refresh flicker, since viewport refreshing would normally
+        // cause the preview pane to be cleared.
+        if (DimpCore.inAjaxCallback) {
+            this.preview_replace = true;
+            this.uid = r.response.newuid;
+            this._stripAttachmentCallback.bind(this, r).defer();
+            return;
+        }
+
+        this.preview_replace = false;
+
+        // Remove old cache value.
+        this._expirePPCache([ this._getPPId(r.olduid, r.oldmbox) ]);
+    },
+
+    // opts = mailbox, uid
+    updateMsgLog: function(log, opts)
+    {
+        var tmp;
+
+        if (!opts ||
+            (this.pp &&
+             this.pp.imapuid == opts.uid &&
+             this.pp.view == opts.mailbox)) {
+            $('msgLogInfo').show();
+
+            if (opts) {
+                $('msgloglist_col').show();
+                $('msgloglist_exp').hide();
+            }
+
+            DimpCore.updateMsgLog(log);
+        }
+
+        if (opts) {
+            tmp = this._getPPId(opts.uid, opts.mailbox);
+            if (this.ppcache[tmp]) {
+                this.ppcache[tmp].response.log = log;
+            }
+        }
+    },
+
+    initPreviewPane: function()
+    {
+        var sel = this.viewport.getSelected();
+        if (sel.size() != 1) {
+            this.clearPreviewPane();
+        } else {
+            this.loadPreview(sel.get('dataob').first());
+        }
+    },
+
+    clearPreviewPane: function()
+    {
+        this.loadingImg('msg', false);
+        $('previewMsg').hide();
+        $('previewPane').scrollTop = 0;
+        $('previewInfo').show();
+        this.pp = null;
+    },
+
+    _toggleHeaders: function(elt, update)
+    {
+        if (update) {
+            DIMP.conf.toggle_pref = !DIMP.conf.toggle_pref;
+            this._updatePrefs('dimp_toggle_headers', Number(elt.id == 'th_expand'));
+        }
+        [ elt.up().select('A'), $('msgHeadersColl', 'msgHeaders') ].flatten().invoke('toggle');
+    },
+
+    _expirePPCache: function(ids)
+    {
+        this.ppfifo = this.ppfifo.diff(ids);
+        ids.each(function(i) {
+            delete this.ppcache[i];
+        }, this);
+
+        if (this.ppfifo.size() > this.ppcachesize) {
+            delete this.ppcache[this.ppfifo.shift()];
+        }
+    },
+
+    _getPPId: function(uid, mailbox)
+    {
+        return uid + '|' + mailbox;
+    },
+
+    // Labeling functions
+    updateSeenUID: function(r, setflag)
+    {
+        var isunseen = !this.hasFlag('\\seen', r),
+            sel, unseen;
+
+        if ((setflag && !isunseen) || (!setflag && isunseen)) {
+            return false;
+        }
+
+        sel = this.viewport.createSelection('dataob', r);
+        unseen = this.getUnseenCount(r.view);
+
+        unseen += setflag ? -1 : 1;
+        this.updateFlag(sel, '\\seen', setflag);
+
+        this.updateUnseenStatus(r.view, unseen);
+    },
+
+    // mbox = (string)
+    getUnseenCount: function(mbox)
+    {
+        var elt = $(this.getFolderId(mbox));
+        return elt ? Number(elt.retrieve('u')) : 0;
+    },
+
+    updateUnseenStatus: function(mbox, unseen)
+    {
+        if (this.viewport) {
+            this.viewport.setMetaData({ unseen: unseen }, mbox);
+        }
+
+        this.setFolderLabel(mbox, unseen);
+
+        if (this.folder == mbox) {
+            this.updateTitle();
+        }
+    },
+
+    setMessageListTitle: function()
+    {
+        var range,
+            rows = this.viewport.getMetaData('total_rows');
+
+        if (rows) {
+            range = this.viewport.currentViewableRange();
+            $('msgHeader').update(DIMP.text.messagetitle.sub('%d', range.first).sub('%d', range.last).sub('%d', rows));
+        } else {
+            $('msgHeader').update(DIMP.text.nomessages);
+        }
+    },
+
+    // f = (string|Element)
+    setFolderLabel: function(f, unseen)
+    {
+        var elt, mbox;
+
+        if (Object.isElement(f)) {
+            mbox = f.retrieve('mbox');
+            elt = f;
+        } else {
+            mbox = f;
+            elt = $(this.getFolderId(f));
+        }
+
+        if (!elt) {
+            return;
+        }
+
+        if (Object.isUndefined(unseen)) {
+            unseen = this.getUnseenCount(mbox);
+        } else {
+            if (Object.isUndefined(elt.retrieve('u')) ||
+                elt.retrieve('u') == unseen) {
+                return;
+            }
+
+            unseen = Number(unseen);
+            elt.store('u', unseen);
+        }
+
+        if (mbox == 'INBOX' && window.fluid) {
+            window.fluid.setDockBadge(unseen ? unseen : '');
+        }
+
+        elt.down('A').update((unseen > 0) ?
+            new Element('STRONG').insert(elt.retrieve('l')).insert('&nbsp;').insert(new Element('SPAN', { className: 'count', dir: 'ltr' }).insert('(' + unseen + ')')) :
+            elt.retrieve('l'));
+    },
+
+    getFolderId: function(f)
+    {
+        return 'fld' + f.gsub('_', '__').gsub(/\W/, '_');
+    },
+
+    getSubFolderId: function(f)
+    {
+        if (f.endsWith('_special')) {
+            f = f.slice(0, -8);
+        }
+        return 'sub_' + f;
+    },
+
+    /* Folder list updates. */
+    poll: function(force)
+    {
+        var args = {},
+            check = 'checkmaillink';
+
+        // Reset poll folder counter.
+        this.setPoll();
+
+        // Check for label info - it is possible that the mailbox may be
+        // loading but not complete yet and sending this request will cause
+        // duplicate info to be returned.
+        if (this.folder &&
+            $('dimpmain_folder').visible() &&
+            this.viewport.getMetaData('label')) {
+            args = this.viewport.addRequestParams({});
+        }
+
+        if (force) {
+            args.set('forceUpdate', 1);
+            check = 'refreshlink';
+        }
+
+        $(check).down('A').update('[' + DIMP.text.check + ']');
+        DimpCore.doAction('poll', args);
+    },
+
+    pollCallback: function(r)
+    {
+        if (r.poll) {
+            $H(r.poll).each(function(u) {
+                this.updateUnseenStatus(u.key, u.value);
+            }, this);
+        }
+
+        if (r.quota) {
+            this._displayQuota(r.quota);
+        }
+
+        $('checkmaillink').down('A').update(DIMP.text.getmail);
+        if ($('refreshlink').visible()) {
+            $('refreshlink').down('A').update(DIMP.text.refresh);
+        }
+    },
+
+    _displayQuota: function(r)
+    {
+        var q = $('quota').cleanWhitespace();
+        q.setText(r.m);
+        q.down('SPAN.used IMG').writeAttribute('width', 99 - r.p);
+    },
+
+    setPoll: function()
+    {
+        if (DIMP.conf.refresh_time) {
+            if (this.pollPE) {
+                this.pollPE.stop();
+            }
+            // Run in anonymous function, or else PeriodicalExecuter passes
+            // in itself as first ('force') parameter to poll().
+            this.pollPE = new PeriodicalExecuter(function() { this.poll(); }.bind(this), DIMP.conf.refresh_time);
+        }
+    },
+
+    _portalCallback: function(r)
+    {
+        if (r.response.linkTags) {
+            var head = $(document.documentElement).down('HEAD');
+            r.response.linkTags.each(function(newLink) {
+                var link = new Element('LINK', { type: 'text/css', rel: 'stylesheet', href: newLink.href });
+                if (newLink.media) {
+                    link.media = newLink.media;
+                }
+                head.insert(link);
+            });
+        }
+        $('dimpmain_portal').update(r.response.portal);
+    },
+
+    /* Search functions. */
+    isSearch: function(id, qsearch)
+    {
+        id = id ? id : this.folder;
+        return id && id.startsWith(DIMP.conf.searchprefix) && (!qsearch || this.search);
+    },
+
+    _quicksearchOnBlur: function()
+    {
+        $('qsearch').removeClassName('qsearchFocus');
+        if (!$F('qsearch_input')) {
+            this._setQsearchText(true);
+        }
+    },
+
+    quicksearchRun: function()
+    {
+        var q = $F('qsearch_input');
+
+        if (this.isSearch()) {
+            /* Search text has changed. */
+            if (this.search.query != q) {
+                this.folderswitch = true;
+            }
+            this.viewport.reload();
+        } else {
+            this.search = {
+                label: this.viewport.getMetaData('label'),
+                mbox: this.folder,
+                query: q
+            };
+            this.loadMailbox(DIMP.conf.qsearchid);
+        }
+    },
+
+    // 'noload' = (boolean) If true, don't load the mailbox
+    quicksearchClear: function(noload)
+    {
+        var f = this.folder;
+
+        if (!$('qsearch').hasClassName('qsearchFocus')) {
+            this._setQsearchText(true);
+        }
+
+        if (this.isSearch()) {
+            this.resetSelected();
+            $('qsearch', 'qsearch_icon', 'qsearch_input').invoke('show');
+            if (!noload) {
+                this.loadMailbox(this.search ? this.search.mbox : 'INBOX');
+            }
+            this.viewport.deleteView(f);
+            this.search = null;
+        }
+    },
+
+    // d = (boolean) Deactivate quicksearch input?
+    _setQsearchText: function(d)
+    {
+        $('qsearch_input').setValue(d ? DIMP.text.search + ' (' + $('ctx_qsearchby_' + DIMP.conf.qsearchfield).getText() + ')' : '');
+        [ $('qsearch') ].invoke(d ? 'removeClassName' : 'addClassName', 'qsearchActive');
+        if ($('qsearch_input').visible()) {
+            $('qsearch_close').hide().next().hide();
+        }
+    },
+
+    // hideall = (boolean) Hide entire searchbox?
+    _quicksearchDeactivate: function(hideall)
+    {
+        if (hideall) {
+            $('qsearch').hide();
+        } else {
+            $('qsearch_close').show().next().show();
+            $('qsearch_icon', 'qsearch_input').invoke('hide');
+        }
+    },
+
+    /* Enable/Disable DIMP action buttons as needed. */
+    toggleButtons: function()
+    {
+        DimpCore.toggleButtons($('dimpmain_folder_top').select('DIV.dimpActions A.noselectDisable'), this.selectedCount() == 0);
+    },
+
+    /* Drag/Drop handler. */
+    folderDropHandler: function(e)
+    {
+        var dropbase, sel, uids,
+            drag = e.memo.element,
+            drop = e.element(),
+            foldername = drop.retrieve('mbox'),
+            ftype = drop.retrieve('ftype');
+
+        if (drag.hasClassName('folder')) {
+            dropbase = (drop == $('dropbase'));
+            if (dropbase ||
+                (ftype != 'special' && !this.isSubfolder(drag, drop))) {
+                DimpCore.doAction('renameMailbox', { old_name: drag.retrieve('mbox'), new_parent: dropbase ? '' : foldername, new_name: drag.retrieve('l') }, { callback: this.mailboxCallback.bind(this) });
+            }
+        } else if (ftype != 'container') {
+            sel = this.viewport.getSelected();
+
+            if (sel.size()) {
+                // Dragging multiple selected messages.
+                uids = sel;
+            } else if (drag.retrieve('mbox') != foldername) {
+                // Dragging a single unselected message.
+                uids = this.viewport.createSelection('domid', drag.id);
+            }
+
+            if (uids.size()) {
+                if (e.memo.dragevent.ctrlKey) {
+                    DimpCore.doAction('copyMessages', this.viewport.addRequestParams({ mboxto: foldername }), { uids: uids });
+                } else if (this.folder != foldername) {
+                    // Don't allow drag/drop to the current folder.
+                    this.updateFlag(uids, '\\deleted', true);
+                    DimpCore.doAction('moveMessages', this.viewport.addRequestParams({ mboxto: foldername }), { uids: uids });
+                }
+            }
+        }
+    },
+
+    dragCaption: function()
+    {
+        var cnt = this.selectedCount();
+        return cnt + ' ' + (cnt == 1 ? DIMP.text.message : DIMP.text.messages);
+    },
+
+    onDragMouseDown: function(e)
+    {
+        var args,
+            elt = e.element(),
+            id = elt.identify(),
+            d = DragDrop.Drags.getDrag(id);
+
+        if (elt.hasClassName('vpRow')) {
+            args = { right: e.memo.isRightClick() };
+            d.selectIfNoDrag = false;
+
+            // Handle selection first.
+            if (DimpCore.DMenu.operaCheck(e)) {
+                if (!this.isSelected('domid', id)) {
+                    this.msgSelect(id, { right: true });
+                }
+            } else if (!args.right && (e.memo.ctrlKey || e.memo.metaKey)) {
+                this.msgSelect(id, $H({ ctrl: true }).merge(args).toObject());
+            } else if (e.memo.shiftKey) {
+                this.msgSelect(id, $H({ shift: true }).merge(args).toObject());
+            } else if (e.memo.element().hasClassName('msCheck')) {
+                this.msgSelect(id, { ctrl: true, right: true });
+            } else if (this.isSelected('domid', id)) {
+                if (!args.right && this.selectedCount()) {
+                    d.selectIfNoDrag = true;
+                }
+            } else {
+                this.msgSelect(id, args);
+            }
+        } else if (elt.hasClassName('folder')) {
+            d.opera = DimpCore.DMenu.operaCheck(e);
+        }
+    },
+
+    onDrag: function(e)
+    {
+        if (e.element().hasClassName('folder')) {
+            var d = e.memo;
+            if (!d.opera && !d.wasDragged) {
+                $('folderopts').hide();
+                $('dropbase').show();
+                d.ghost.removeClassName('on');
+            }
+        }
+    },
+
+    onDragEnd: function(e)
+    {
+        var elt = e.element(),
+            id = elt.identify(),
+            d = DragDrop.Drags.getDrag(id);
+
+        if (elt.hasClassName('folder')) {
+            if (!d.opera) {
+                $('folderopts').show();
+                $('dropbase').hide();
+            }
+        } else if (elt.hasClassName('splitBarVertSidebar')) {
+            $('sidebar').setStyle({ width: d.lastCoord[0] + 'px' });
+            elt.setStyle({ left: $('sidebar').clientWidth + 'px' });
+            $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + elt.clientWidth) + 'px' });
+        }
+    },
+
+    onDragMouseUp: function(e)
+    {
+        var elt = e.element(),
+            id = elt.identify();
+
+        if (elt.hasClassName('vpRow') &&
+            DragDrop.Drags.getDrag(id).selectIfNoDrag) {
+            this.msgSelect(id, { right: e.memo.isRightClick() });
+        }
+    },
+
+    /* Keydown event handler */
+    keydownHandler: function(e)
+    {
+        var all, cnt, co, form, h, need, pp, ps, r, row, rownum, rowoff, sel,
+            tmp, vsel,
+            elt = e.element(),
+            kc = e.keyCode || e.charCode;
+
+        // Only catch keyboard shortcuts in message list view.
+        if (!$('dimpmain_folder').visible()) {
+            return;
+        }
+
+        // Form catching - normally we will ignore, but certain cases we want
+        // to catch.
+        form = e.findElement('FORM');
+        if (form) {
+            switch (kc) {
+            case Event.KEY_ESC:
+            case Event.KEY_TAB:
+                // Catch escapes in search box
+                if (elt.readAttribute('id') == 'qsearch_input') {
+                    if (kc == Event.KEY_ESC || !elt.getValue()) {
+                        this.quicksearchClear();
+                    }
+                    elt.blur();
+                    e.stop();
+                }
+                break;
+
+            case Event.KEY_RETURN:
+                // Catch returns in RedBox
+                if (form.readAttribute('id') == 'RB_folder') {
+                    this.cfolderaction(e);
+                    e.stop();
+                } else if (elt.readAttribute('id') == 'qsearch_input') {
+                    if ($F('qsearch_input')) {
+                        this.quicksearchRun();
+                    } else {
+                        this.quicksearchClear();
+                    }
+                    e.stop();
+                }
+                break;
+
+            default:
+                if (elt.readAttribute('id') == 'qsearch_input') {
+                    $('qsearch_close').show();
+                }
+                break;
+            }
+
+            return;
+        }
+
+        sel = this.viewport.getSelected();
+
+        switch (kc) {
+        case Event.KEY_DELETE:
+        case Event.KEY_BACKSPACE:
+            r = sel.get('dataob');
+            if (e.shiftKey) {
+                this.moveSelected((r.last().VP_rownum == this.viewport.getMetaData('total_rows')) ? (r.first().VP_rownum - 1) : (r.last().VP_rownum + 1), true);
+            }
+            this.deleteMsg({ vs: sel });
+            e.stop();
+            break;
+
+        case Event.KEY_UP:
+        case Event.KEY_DOWN:
+            if (e.shiftKey && this.lastrow != -1) {
+                row = this.viewport.createSelection('rownum', this.lastrow + ((kc == Event.KEY_UP) ? -1 : 1));
+                if (row.size()) {
+                    row = row.get('dataob').first();
+                    this.viewport.scrollTo(row.VP_rownum);
+                    this.msgSelect(row.VP_domid, { shift: true });
+                }
+            } else {
+                this.moveSelected(kc == Event.KEY_UP ? -1 : 1);
+            }
+            e.stop();
+            break;
+
+        case Event.KEY_PAGEUP:
+        case Event.KEY_PAGEDOWN:
+            if (e.altKey) {
+                pp = $('previewPane');
+                h = pp.getHeight();
+                if (h != pp.scrollHeight) {
+                    switch (kc) {
+                    case Event.KEY_PAGEUP:
+                        pp.scrollTop = Math.max(pp.scrollTop - h, 0);
+                        break;
+
+                    case Event.KEY_PAGEDOWN:
+                        pp.scrollTop = Math.min(pp.scrollTop + h, pp.scrollHeight - h + 1);
+                        break;
+                    }
+                }
+                e.stop();
+            } else if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
+                ps = this.viewport.getPageSize() - 1;
+                move = ps * (kc == Event.KEY_PAGEUP ? -1 : 1);
+                if (sel.size() == 1) {
+                    co = this.viewport.currentOffset();
+                    rowoff = sel.get('rownum').first() - 1;
+                    switch (kc) {
+                    case Event.KEY_PAGEUP:
+                        if (co != rowoff) {
+                            move = co - rowoff;
+                        }
+                        break;
+
+                    case Event.KEY_PAGEDOWN:
+                        if ((co + ps) != rowoff) {
+                            move = co + ps - rowoff;
+                        }
+                        break;
+                    }
+                }
+                this.moveSelected(move);
+                e.stop();
+            }
+            break;
+
+        case Event.KEY_HOME:
+        case Event.KEY_END:
+            this.moveSelected(kc == Event.KEY_HOME ? 1 : this.viewport.getMetaData('total_rows'), true);
+            e.stop();
+            break;
+
+        case Event.KEY_RETURN:
+            if (!elt.match('input')) {
+                // Popup message window if single message is selected.
+                if (sel.size() == 1) {
+                    this.msgWindow(sel.get('dataob').first());
+                }
+            }
+            e.stop();
+            break;
+
+        case 65: // A
+        case 97: // a
+            if (e.ctrlKey) {
+                this.selectAll();
+                e.stop();
+            }
+            break;
+
+        case 78: // N
+        case 110: // n
+            if (e.shiftKey && !this.isSearch(this.folder)) {
+                cnt = this.getUnseenCount(this.folder);
+                if (Object.isUndefined(cnt) || cnt) {
+                    vsel = this.viewport.getSelection();
+                    row = vsel.search({ flag: { include: '\\seen' } }).get('rownum');
+                    all = (vsel.size() == this.viewport.getMetaData('total_rows'));
+
+                    if (all ||
+                        (!Object.isUndefined(cnt) && row.size() == cnt)) {
+                        // Here we either have the entire mailbox in buffer,
+                        // or all unseen messages are in the buffer.
+                        if (sel.size()) {
+                            tmp = sel.get('rownum').last();
+                            if (tmp) {
+                                rownum = row.detect(function(r) {
+                                    return tmp < r;
+                                });
+                            }
+                        } else {
+                            rownum = tmp = row.first();
+                        }
+                    } else {
+                        // Here there is no guarantee that the next unseen
+                        // message will appear in the current buffer. Need to
+                        // determine if any gaps are between last selected
+                        // message and next unseen message in buffer.
+                        vsel = vsel.get('rownum');
+
+                        if (sel.size()) {
+                            // We know that the selected rows are in the
+                            // buffer.
+                            tmp = sel.get('rownum').last();
+                        } else if (vsel.include(1)) {
+                            // If no selected rows, start searching from the
+                            // first entry.
+                            tmp = 0;
+                        } else {
+                            // First message is not in current buffer.
+                            need = true;
+                        }
+
+                        if (!need) {
+                            rownum = vsel.detect(function(r) {
+                                if (r > tmp) {
+                                    if (++tmp != r) {
+                                        // We have found a gap.
+                                        need = true;
+                                        throw $break;
+                                    }
+                                    return row.include(tmp);
+                                }
+                            });
+
+                            if (!need && !rownum) {
+                                need = (tmp !== this.viewport.getMetaData('total_rows'));
+                            }
+                        }
+
+                        if (need) {
+                            this.viewport.select(null, { search: { unseen: 1 } });
+                        }
+                    }
+
+                    if (rownum) {
+                        this.moveSelected(rownum, true);
+                    }
+                }
+                e.stop();
+            }
+            break;
+        }
+    },
+
+    dblclickHandler: function(e)
+    {
+        if (e.isRightClick()) {
+            return;
+        }
+
+        var elt = e.element(),
+            tmp;
+
+        if (!elt.hasClassName('vpRow')) {
+            elt = elt.up('.vpRow');
+        }
+
+        if (elt) {
+            tmp = this.viewport.createSelection('domid', elt.identify()).get('dataob').first();
+            if (tmp.draft && this.viewport.getMetaData('drafts')) {
+                DimpCore.compose('resume', { folder: tmp.view, uid: tmp.imapuid })
+            } else {
+                this.msgWindow(tmp);
+            }
+            e.stop();
+        }
+    },
+
+    clickHandler: function(parentfunc, e)
+    {
+        if (e.isRightClick() || DimpCore.DMenu.operaCheck(e)) {
+            return;
+        }
+
+        var elt = e.element(),
+            id, tmp;
+
+        while (Object.isElement(elt)) {
+            id = elt.readAttribute('id');
+
+            switch (id) {
+            case 'normalfolders':
+            case 'specialfolders':
+                this._handleFolderMouseClick(e);
+                break;
+
+            case 'hometab':
+            case 'logolink':
+                this.go('portal');
+                e.stop();
+                return;
+
+            case 'button_compose':
+            case 'composelink':
+                DimpCore.compose('new');
+                e.stop();
+                return;
+
+            case 'checkmaillink':
+            case 'refreshlink':
+                this.poll(id == 'refreshlink');
+                e.stop();
+                return;
+
+            case 'alertsloglink':
+                DimpCore.Growler.toggleLog();
+                $('alertsloglink').down('A').update(DimpCore.Growler.logVisible() ? DIMP.text.hidealog : DIMP.text.showalog);
+                break;
+
+            case 'applyfilterlink':
+                if (this.viewport) {
+                    this.viewport.reload({ applyfilter: 1 });
+                }
+                e.stop();
+                return;
+
+            case 'appportal':
+            case 'appoptions':
+                this.go(id.substring(3));
+                e.stop();
+                return;
+
+            case 'applogout':
+                elt.down('A').update('[' + DIMP.text.onlogout + ']');
+                DimpCore.logout();
+                e.stop();
+                return;
+
+            case 'button_forward':
+            case 'button_reply':
+                this.composeMailbox(id == 'button_reply' ? 'reply_auto' : 'forward_auto');
+                break;
+
+            case 'button_ham':
+            case 'button_spam':
+                this.reportSpam(id == 'button_spam');
+                e.stop();
+                return;
+
+            case 'button_deleted':
+                this.deleteMsg();
+                e.stop();
+                return;
+
+            case 'msglistHeader':
+                this.sort(e.element().retrieve('sortby'));
+                e.stop();
+                return;
+
+            case 'th_expand':
+            case 'th_collapse':
+                this._toggleHeaders(elt, true);
+                break;
+
+            case 'msgloglist_toggle':
+            case 'partlist_toggle':
+                tmp = (id == 'partlist_toggle') ? 'partlist' : 'msgloglist';
+                $(tmp + '_col', tmp + '_exp').invoke('toggle');
+                Effect.toggle(tmp, 'blind', {
+                    duration: 0.2,
+                    queue: {
+                        position: 'end',
+                        scope: tmp,
+                        limit: 2
+                    }
+                });
+                break;
+
+            case 'msg_newwin':
+            case 'msg_newwin_options':
+                this.msgWindow(this.viewport.getSelection().search({ imapuid: { equal: [ this.pp.imapuid ] } , view: { equal: [ this.pp.view ] } }).get('dataob').first());
+                e.stop();
+                return;
+
+            case 'msg_view_source':
+                DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: this.pp.imapuid, mailbox: this.pp.view, actionID: 'view_source', id: 0 }, true), this.pp.imapuid + '|' + this.pp.view);
+                break;
+
+            case 'applicationfolders':
+                tmp = e.element();
+                if (!tmp.hasClassName('custom')) {
+                    tmp = tmp.up('LI.custom');
+                }
+                if (tmp) {
+                    this.go('app:' + tmp.down('A').identify().substring(3));
+                    e.stop();
+                    return;
+                }
+                break;
+
+            case 'tabbar':
+                if (e.element().hasClassName('applicationtab')) {
+                    this.go('app:' + e.element().identify().substring(6));
+                    e.stop();
+                    return;
+                }
+                break;
+
+            case 'dimpmain_portal':
+                if (e.element().match('H1.header a')) {
+                    this.go('app:' + e.element().readAttribute('app'));
+                    e.stop();
+                    return;
+                }
+                break;
+
+            case 'qsearch':
+                if (e.element().readAttribute('id') != 'qsearch_icon') {
+                    elt.addClassName('qsearchFocus');
+                    if (!elt.hasClassName('qsearchActive')) {
+                        this._setQsearchText(false);
+                    }
+                    $('qsearch_input').focus();
+                }
+                break;
+
+            case 'qsearch_close':
+            case 'qsearch_close_filter':
+                this.quicksearchClear();
+                e.stop();
+                return;
+
+            default:
+                if (elt.hasClassName('RBFolderOk')) {
+                    this.cfolderaction(e);
+                    e.stop();
+                    return;
+                } else if (elt.hasClassName('RBFolderCancel')) {
+                    this._closeRedBox();
+                    e.stop();
+                    return;
+                } else if (elt.hasClassName('printAtc')) {
+                    DimpCore.popupWindow(DimpCore.addURLParam(DIMP.conf.URI_VIEW, { uid: this.pp.imapuid, mailbox: this.pp.view, actionID: 'print_attach', id: elt.readAttribute('mimeid') }, true), this.pp.imapuid + '|' + this.pp.view + '|print', IMP.printWindow);
+                    e.stop();
+                    return;
+                } else if (elt.hasClassName('stripAtc')) {
+                    this.loadingImg('msg', true);
+                    DimpCore.doAction('stripAttachment', this.viewport.addRequestParams({ id: elt.readAttribute('mimeid') }), { uids: this.viewport.createSelection('dataob', this.pp), callback: this._stripAttachmentCallback.bind(this) });
+                    e.stop();
+                    return;
+                }
+            }
+
+            elt = elt.up();
+        }
+
+        parentfunc(e);
+    },
+
+    mouseoverHandler: function(e)
+    {
+        if (DragDrop.Drags.drag) {
+            var elt = e.element();
+            if (elt.hasClassName('exp')) {
+                this._toggleSubFolder(elt.up(), 'tog');
+            }
+        }
+    },
+
+    changeHandler: function(e)
+    {
+        var elt = e.element();
+
+        if (elt.readAttribute('name') == 'search_criteria' &&
+            elt.descendantOf('RB_window')) {
+            [ elt.next() ].invoke($F(elt) ? 'show' : 'hide');
+            RedBox.setWindowPosition();
+        }
+    },
+
+    /* Handle rename folder actions. */
+    renameFolder: function(folder)
+    {
+        if (Object.isUndefined(folder)) {
+            return;
+        }
+
+        folder = $(folder);
+        var n = this._createFolderForm(this._folderAction.bindAsEventListener(this, folder, 'rename'), DIMP.text.rename_prompt);
+        n.down('input').setValue(folder.retrieve('l'));
+    },
+
+    /* Handle insert folder actions. */
+    createBaseFolder: function()
+    {
+        this._createFolderForm(this._folderAction.bindAsEventListener(this, '', 'create'), DIMP.text.create_prompt);
+    },
+
+    createSubFolder: function(folder)
+    {
+        if (!Object.isUndefined(folder)) {
+            this._createFolderForm(this._folderAction.bindAsEventListener(this, $(folder), 'createsub'), DIMP.text.createsub_prompt);
+        }
+    },
+
+    _createFolderForm: function(action, text)
+    {
+        var n = $($('folderform').down().cloneNode(true)).writeAttribute('id', 'RB_folder');
+        n.down('P').insert(text);
+
+        this.cfolderaction = action;
+
+        RedBox.overlay = true;
+        RedBox.onDisplay = Form.focusFirstElement.curry(n);
+        RedBox.showHtml(n);
+        return n;
+    },
+
+    _closeRedBox: function()
+    {
+        RedBox.close();
+        this.cfolderaction = null;
+    },
+
+    _folderAction: function(e, folder, mode)
+    {
+        this._closeRedBox();
+
+        var action, params, val,
+            form = e.findElement('form');
+        val = $F(form.down('input'));
+
+        if (val) {
+            switch (mode) {
+            case 'rename':
+                folder = folder.up('LI');
+                if (folder.retrieve('l') != val) {
+                    action = 'renameMailbox';
+                    params = {
+                        old_name: folder.retrieve('mbox'),
+                        new_parent: folder.up().hasClassName('folderlist') ? '' : folder.up(1).previous().retrieve('mbox'),
+                        new_name: val
+                    };
+                }
+                break;
+
+            case 'create':
+            case 'createsub':
+                action = 'createMailbox';
+                params = { mbox: val };
+                if (mode == 'createsub') {
+                    params.parent = folder.up('LI').retrieve('mbox');
+                }
+                break;
+            }
+
+            if (action) {
+                DimpCore.doAction(action, params, { callback: this.mailboxCallback.bind(this) });
+            }
+        }
+    },
+
+    /* Mailbox action callback functions. */
+    mailboxCallback: function(r)
+    {
+        r = r.response.mailbox;
+
+        if (r.d) {
+            r.d.each(this.deleteFolder.bind(this));
+        }
+        if (r.c) {
+            r.c.each(this.changeFolder.bind(this));
+        }
+        if (r.a) {
+            r.a.each(this.createFolder.bind(this));
+        }
+    },
+
+    deleteCallback: function(r)
+    {
+        var search = null, uids = [], vs;
+
+        if (!r.deleted) {
+            return;
+        }
+
+        this.loadingImg('viewport', false);
+
+        r = r.deleted;
+        if (!r.uids || r.mbox != this.folder) {
+            return;
+        }
+        r.uids = DimpCore.parseRangeString(r.uids);
+
+        // Need to convert uid list to listing of unique viewport IDs since
+        // we may be dealing with multiple mailboxes (i.e. virtual folders)
+        vs = this.viewport.getSelection(this.folder);
+        if (vs.getBuffer().getMetaData('search')) {
+            $H(r.uids).each(function(pair) {
+                pair.value.each(function(v) {
+                    uids.push(pair.key + DIMP.conf.IDX_SEP + v);
+                });
+            });
+
+            search = this.viewport.getSelection().search({ VP_id: { equal: uids } });
+        } else {
+            r.uids = r.uids[this.folder];
+            r.uids.each(function(f, u) {
+                uids.push(u + f);
+            }.curry(this.folder));
+            search = this.viewport.createSelection('uid', r.uids);
+        }
+
+        if (search.size()) {
+            if (r.remove) {
+                this.viewport.remove(search, { noupdate: r.ViewPort });
+                this._expirePPCache(uids);
+            } else {
+                // Need this to catch spam deletions.
+                this.updateFlag(search, '\\deleted', true);
+            }
+        }
+    },
+
+    _emptyMailboxCallback: function(r)
+    {
+        if (r.response.mbox) {
+            if (this.folder == r.response.mbox) {
+                this.viewport.reload();
+                this.clearPreviewPane();
+            } else {
+                this.viewport.deleteView(r.response.mbox);
+            }
+            this.setFolderLabel(r.response.mbox, 0);
+        }
+    },
+
+    _flagAllCallback: function(r)
+    {
+        if (r.response &&
+            r.response.mbox == this.folder) {
+            r.response.flags.each(function(f) {
+                this.updateFlag(this.viewport.createSelection('rownum', this.viewport.getAllRows()), f, r.response.set);
+            }, this);
+        }
+    },
+
+    _folderLoadCallback: function(r, callback)
+    {
+        this.mailboxCallback(r);
+
+        if (callback) {
+            callback();
+        }
+
+        if (this.folder) {
+            this.highlightSidebar(this.getFolderId(this.folder));
+        }
+
+        $('foldersLoading').hide();
+        $('foldersSidebar').show();
+
+        if ($('normalfolders').getStyle('max-height') !== null) {
+            this._sizeFolderlist();
+        }
+
+        if (r.response.quota) {
+            this._displayQuota(r.response.quota);
+        }
+    },
+
+    _handleFolderMouseClick: function(e)
+    {
+        var elt = e.element(),
+            li = elt.match('LI') ? elt : elt.up('LI');
+
+        if (!li) {
+            return;
+        }
+
+        if (elt.hasClassName('exp') || elt.hasClassName('col')) {
+            this._toggleSubFolder(li, 'tog');
+        } else {
+            switch (li.retrieve('ftype')) {
+            case 'container':
+            case 'scontainer':
+                e.stop();
+                break;
+
+            case 'folder':
+            case 'special':
+            case 'virtual':
+                e.stop();
+                return this.go('folder:' + li.retrieve('mbox'));
+            }
+        }
+    },
+
+    _toggleSubFolder: function(base, mode, noeffect)
+    {
+        var need = [], subs = [];
+
+        if (mode == 'expall' || mode == 'colall') {
+            if (base.hasClassName('subfolders')) {
+                subs.push(base);
+            }
+            subs = subs.concat(base.select('.subfolders'));
+        } else if (mode == 'exp') {
+            // If we are explicitly expanding ('exp'), make sure all parent
+            // subfolders are expanded.
+            // The last 2 elements of ancestors() are the BODY and HTML tags -
+            // don't need to parse through them.
+            subs = base.ancestors().slice(0, -2).reverse().findAll(function(n) { return n.hasClassName('subfolders'); });
+        } else {
+            subs = [ base.next('.subfolders') ];
+        }
+
+        if (!subs) {
+            return;
+        }
+
+        if (mode == 'tog' || mode == 'expall') {
+            subs.compact().each(function(s) {
+                if (!s.visible() && !s.down().childElements().size()) {
+                    need.push(s.previous().retrieve('mbox'));
+                }
+            });
+
+            if (need.size()) {
+                if (mode == 'tog') {
+                    base.down('A').update(DIMP.text.loading);
+                }
+                this._listFolders({
+                    all: Number(mode == 'expall'),
+                    callback: this._toggleSubFolder.bind(this, base, mode, noeffect),
+                    mboxes: need
+                });
+                return;
+            } else if (mode == 'tog') {
+                // Need to pass element here, since we might be working
+                // with 'special' folders.
+                this.setFolderLabel(base);
+            }
+        }
+
+        subs.each(function(s) {
+            if (mode == 'tog' ||
+                ((mode == 'exp' || mode == 'expall') && !s.visible()) ||
+                ((mode == 'col' || mode == 'colall') && s.visible())) {
+                s.previous().down().toggleClassName('exp').toggleClassName('col');
+
+                if (noeffect) {
+                    s.toggle();
+                } else {
+                    Effect.toggle(s, 'blind', {
+                        duration: 0.2,
+                        queue: {
+                            position: 'end',
+                            scope: 'subfolder'
+                        }
+                    });
+                }
+            }
+        });
+    },
+
+    _listFolders: function(params)
+    {
+        var cback;
+
+        params = params || {};
+        params.unsub = Number(this.showunsub);
+        if (!Object.isArray(params.mboxes)) {
+            params.mboxes = [ params.mboxes ];
+        }
+        params.mboxes = params.mboxes.toJSON();
+
+        if (params.callback) {
+            cback = function(func, r) { this._folderLoadCallback(r, func); }.bind(this, params.callback);
+            delete params.callback;
+        } else {
+            cback = this._folderLoadCallback.bind(this);
+        }
+
+        DimpCore.doAction('listMailboxes', params, { callback: cback });
+    },
+
+    // Folder actions.
+    // For format of the ob object, see IMP_Dimp::_createFolderElt().
+    createFolder: function(ob)
+    {
+        var div, f_node, ftype, li, ll, parent_e, tmp,
+            cname = 'container',
+            fid = this.getFolderId(ob.m),
+            label = ob.l || ob.m,
+            mbox = ob.m,
+            submboxid = this.getSubFolderId(fid),
+            submbox = $(submboxid),
+            title = ob.t || ob.m;
+
+        if ($(fid)) {
+            return;
+        }
+
+        if (ob.v) {
+            ftype = ob.co ? 'scontainer' : 'virtual';
+            title = label;
+        } else if (ob.co) {
+            if (ob.n) {
+                ftype = 'scontainer';
+                title = label;
+            } else {
+                ftype = 'container';
+            }
+
+            /* This is a dummy container element to display child elements of
+             * a mailbox displayed in the 'specialfolders' section. */
+            if (ob.dummy) {
+                fid += '_special';
+                cname += ' specialContainer';
+            }
+        } else {
+            cname = 'folder';
+            ftype = ob.s ? 'special' : 'folder';
+        }
+
+        if (ob.un && this.showunsub) {
+            cname += ' unsubFolder';
+        }
+
+        div = new Element('SPAN', { className: 'iconSpan' });
+        if (ob.i) {
+            div.setStyle({ backgroundImage: 'url("' + ob.i + '")' });
+        }
+
+        li = new Element('LI', { className: cname, id: fid, title: title }).store('l', label).store('mbox', mbox).insert(div).insert(new Element('A').insert(label));
+
+        // Now walk through the parent <ul> to find the right place to
+        // insert the new folder.
+        if (submbox) {
+            if (submbox.insert({ before: li }).visible()) {
+                // If an expanded parent mailbox was deleted, we need to toggle
+                // the icon accordingly.
+                div.addClassName('col');
+            }
+        } else {
+            div.addClassName(ob.ch ? 'exp' : (ob.cl || 'folderImg'));
+
+            if (ob.s) {
+                parent_e = $('specialfolders');
+
+                /* Create a dummy container element in 'normalfolders'
+                 * section. */
+                if (ob.ch) {
+                    div.removeClassName('exp').addClassName(ob.cl || 'folderImg');
+
+                    tmp = Object.clone(ob);
+                    tmp.co = tmp.dummy = true;
+                    tmp.s = false;
+                    this.createFolder(tmp);
+                }
+            } else {
+                parent_e = ob.pa
+                    ? $(this.getSubFolderId(this.getFolderId(ob.pa))).down()
+                    : $('normalfolders');
+            }
+
+            /* Virtual folders are sorted on the server. */
+            if (!ob.v) {
+                ll = mbox.toLowerCase();
+                f_node = parent_e.childElements().find(function(node) {
+                    var nodembox = node.retrieve('mbox');
+                    return nodembox &&
+                           (!ob.s || nodembox != 'INBOX') &&
+                           (ll < nodembox.toLowerCase());
+                });
+            }
+
+            if (f_node) {
+                f_node.insert({ before: li });
+            } else {
+                parent_e.insert(li);
+            }
+
+            // Make sure the sub<mbox> ul is created if necessary.
+            if (!ob.s && ob.ch) {
+                li.insert({ after: new Element('LI', { className: 'subfolders', id: submboxid }).insert(new Element('UL')).hide() });
+            }
+        }
+
+        li.store('ftype', ftype);
+
+        // Make the new folder a drop target.
+        if (!ob.v) {
+            new Drop(li, this._folderDropConfig);
+        }
+
+        // Check for unseen messages
+        if (ob.po) {
+            li.store('u', '');
+            this.setFolderLabel(mbox, ob.u);
+        }
+
+        switch (ftype) {
+        case 'special':
+            // For purposes of the contextmenu, treat special folders
+            // like regular folders.
+            ftype = 'folder';
+            // Fall through.
+
+        case 'container':
+        case 'folder':
+            new Drag(li, this._folderDragConfig);
+            DimpCore.addContextMenu({
+                id: fid,
+                type: ftype
+            });
+            break;
+
+        case 'scontainer':
+        case 'virtual':
+            DimpCore.addContextMenu({
+                id: fid,
+                type: (ob.v == 2) ? 'vfolder' : 'noactions'
+            });
+            break;
+        }
+    },
+
+    deleteFolder: function(folder)
+    {
+        if (this.folder == folder) {
+            this.go('folder:INBOX');
+        }
+        this.deleteFolderElt(this.getFolderId(folder), true);
+    },
+
+    changeFolder: function(ob)
+    {
+        var fdiv, oldexpand,
+            fid = this.getFolderId(ob.m);
+
+        if ($(fid + '_special')) {
+            // The case of children being added to a special folder is
+            // handled by createFolder().
+            if (!ob.ch) {
+                this.deleteFolderElt(fid + '_special', true);
+            }
+            return;
+        }
+
+        fdiv = $(fid).down('DIV');
+        oldexpand = fdiv && fdiv.hasClassName('col');
+
+        this.deleteFolderElt(fid, !ob.ch);
+        if (ob.co && this.folder == ob.m) {
+            this.go('folder:INBOX');
+        }
+        this.createFolder(ob);
+        if (ob.ch && oldexpand) {
+            fdiv.removeClassName('exp').addClassName('col');
+        }
+    },
+
+    deleteFolderElt: function(fid, sub)
+    {
+        var f = $(fid), submbox;
+        if (!f) {
+            return;
+        }
+
+        if (sub) {
+            submbox = $(this.getSubFolderId(fid));
+            if (submbox) {
+                submbox.remove();
+            }
+        }
+        [ DragDrop.Drags.getDrag(fid), DragDrop.Drops.getDrop(fid) ].compact().invoke('destroy');
+        this._removeMouseEvents(f);
+        if (this.viewport) {
+            this.viewport.deleteView(fid);
+        }
+        f.remove();
+    },
+
+    _sizeFolderlist: function()
+    {
+        var nf = $('normalfolders');
+        nf.setStyle({ height: (document.viewport.getHeight() - nf.cumulativeOffset()[1]) + 'px' });
+    },
+
+    toggleSubscribed: function()
+    {
+        this.showunsub = !this.showunsub;
+        $('ctx_folderopts_sub', 'ctx_folderopts_unsub').invoke('toggle');
+        this._reloadFolders();
+    },
+
+    _reloadFolders: function()
+    {
+        $('foldersLoading').show();
+        $('foldersSidebar').hide();
+
+        [ $('specialfolders').childElements(), $('dropbase').nextSiblings() ].flatten().each(function(elt) {
+            this.deleteFolderElt(elt.readAttribute('id'), true);
+        }, this);
+
+        this._listFolders({ reload: 1, mboxes: this.folder });
+    },
+
+    subscribeFolder: function(f, sub)
+    {
+        var fid = this.getFolderId(f);
+        DimpCore.doAction('subscribe', { mbox: f, sub: Number(sub) });
+
+        if (this.showunsub) {
+            [ $(fid) ].invoke(sub ? 'removeClassName' : 'addClassName', 'unsubFolder');
+        } else if (!sub) {
+            this.deleteFolderElt(fid);
+        }
+    },
+
+    /* Flag actions for message list. */
+    _getFlagSelection: function(opts)
+    {
+        var vs;
+
+        if (opts.vs) {
+            vs = opts.vs;
+        } else if (opts.uid) {
+            vs = opts.mailbox
+                ? this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ opts.uid ] }, view: { equal: [ opts.mailbox ] } })
+                : this.viewport.createSelection('dataob', opts.uid);
+        } else {
+            vs = this.viewport.getSelected();
+        }
+
+        return vs;
+    },
+
+    _doMsgAction: function(type, opts, args)
+    {
+        var vs = this._getFlagSelection(opts);
+
+        if (vs.size()) {
+            // This needs to be synchronous Ajax if we are calling from a
+            // popup window because Mozilla will not correctly call the
+            // callback function if the calling window has been closed.
+            DimpCore.doAction(type, this.viewport.addRequestParams(args), { uids: vs, ajaxopts: { asynchronous: !(opts.uid && opts.mailbox) } });
+            return vs;
+        }
+
+        return false;
+    },
+
+    // spam = (boolean) True for spam, false for innocent
+    // opts = 'mailbox', 'uid'
+    reportSpam: function(spam, opts)
+    {
+        opts = opts || {};
+        if (this._doMsgAction('reportSpam', opts, { spam: Number(spam) })) {
+            // Indicate to the user that something is happening (since spam
+            // reporting may not be instantaneous).
+            this.loadingImg('viewport', true);
+        }
+    },
+
+    // blacklist = (boolean) True for blacklist, false for whitelist
+    // opts = 'mailbox', 'uid'
+    blacklist: function(blacklist, opts)
+    {
+        opts = opts || {};
+        this._doMsgAction('blacklist', opts, { blacklist: blacklist });
+    },
+
+    // opts = 'mailbox', 'uid'
+    deleteMsg: function(opts)
+    {
+        opts = opts || {};
+        var vs = this._getFlagSelection(opts);
+
+        // Make sure that any given row is not deleted more than once. Need to
+        // explicitly mark here because message may already be flagged deleted
+        // when we load page (i.e. switching to using trash folder).
+        vs = vs.search({ isdel: { notequal: [ true ] } });
+        if (!vs.size()) {
+            return;
+        }
+        vs.set({ isdel: true });
+
+        opts.vs = vs;
+
+        this._doMsgAction('deleteMessages', opts, {});
+        this.updateFlag(vs, '\\deleted', true);
+    },
+
+    // flag = (string) IMAP flag name
+    // set = (boolean) True to set flag
+    // opts = (Object) 'mailbox', 'noserver', 'uid'
+    flag: function(flag, set, opts)
+    {
+        opts = opts || {};
+        var flags = [ (set ? '' : '-') + flag ],
+            vs = this._getFlagSelection(opts);
+
+        if (!vs.size()) {
+            return;
+        }
+
+        switch (flag) {
+        case '\\answered':
+            if (set) {
+                this.updateFlag(vs, '\\flagged', false);
+                flags.push('-\\flagged');
+            }
+            break;
+
+        case '\\deleted':
+            vs.set({ isdel: false });
+            break;
+
+        case '\\seen':
+            vs.get('dataob').each(function(s) {
+                this.updateSeenUID(s, set);
+            }, this);
+            break;
+        }
+
+        this.updateFlag(vs, flag, set);
+        if (!opts.noserver) {
+            DimpCore.doAction('flagMessages', this.viewport.addRequestParams({ flags: flags.toJSON(), view: this.folder }), { uids: vs });
+        }
+    },
+
+    // type = (string) 'seen' or 'unseen'
+    // mbox = (string) The mailbox to flag
+    flagAll: function(type, set, mbox)
+    {
+        DimpCore.doAction('flagAll', { flags: [ type ].toJSON(), set: Number(set), mbox: mbox }, { callback: this._flagAllCallback.bind(this) });
+    },
+
+    hasFlag: function(f, r)
+    {
+        return this.convertFlag(f, r.flag ? r.flag.include(f) : false);
+    },
+
+    convertFlag: function(f, set)
+    {
+        /* For some flags, we need to do an inverse match (e.g. knowing a
+         * message is SEEN is not as important as knowing the message lacks
+         * the SEEN FLAG). This function will determine if, for a given flag,
+         * the inverse action should be taken on it. */
+        return DIMP.conf.flags[f].n ? !set : set;
+    },
+
+    updateFlag: function(vs, flag, add)
+    {
+        var s = {};
+        add = this.convertFlag(flag, add);
+
+        vs.get('dataob').each(function(ob) {
+            this._updateFlag(ob, flag, add);
+
+            if (this.isSearch()) {
+                if (s[ob.view]) {
+                    s[ob.view].push(ob.imapuid);
+                } else {
+                    s[ob.view] = [ ob.imapuid ];
+                }
+            }
+        }, this);
+
+        /* If this is a search mailbox, also need to update flag in base view,
+         * if it is in the buffer. */
+        $H(s).each(function(m) {
+            var tmp = this.viewport.getSelection(m.key).search({ imapuid: { equal: m.value }, view: { equal: m.key } });
+            if (tmp.size()) {
+                this._updateFlag(tmp.get('dataob').first(), flag, add);
+            }
+        }, this);
+    },
+
+    _updateFlag: function(ob, flag, add)
+    {
+        ob.flag = ob.flag
+            ? ob.flag.without(flag)
+            : [];
+
+        if (add) {
+            ob.flag.push(flag);
+        }
+
+        this.viewport.updateRow(ob);
+    },
+
+    /* Miscellaneous folder actions. */
+    purgeDeleted: function()
+    {
+        DimpCore.doAction('purgeDeleted', this.viewport.addRequestParams({}));
+    },
+
+    modifyPoll: function(folder, add)
+    {
+        DimpCore.doAction('modifyPoll', { add: Number(add), mbox: folder }, { callback: this._modifyPollCallback.bind(this) });
+    },
+
+    _modifyPollCallback: function(r)
+    {
+        r = r.response;
+        var f = r.mbox, fid, p = { response: { poll: {} } };
+        fid = $(this.getFolderId(f));
+
+        if (r.add) {
+            p.response.poll[f] = r.poll.u;
+            fid.store('u', 0);
+        } else {
+            p.response.poll[f] = 0;
+        }
+
+        if (!r.add) {
+            fid.store('u', null);
+            this.updateUnseenStatus(f, 0);
+        }
+    },
+
+    loadingImg: function(id, show)
+    {
+        DimpCore.loadingImg(id + 'Loading', id == 'viewport' ? 'msgSplitPane' : 'previewPane', show);
+    },
+
+    // p = (element) Parent element
+    // c = (element) Child element
+    isSubfolder: function(p, c)
+    {
+        var sf = $(this.getSubFolderId(p.identify()));
+        return sf && c.descendantOf(sf);
+    },
+
+    /* Pref updating function. */
+    _updatePrefs: function(pref, value)
+    {
+        new Ajax.Request(DimpCore.addURLParam(DIMP.conf.URI_PREFS), { parameters: { pref: pref, value: value } });
+    },
+
+    /* Onload function. */
+    onDomLoad: function()
+    {
+        DimpCore.init();
+
+        var DM = DimpCore.DMenu, tmp;
+
+        /* Register global handlers now. */
+        document.observe('keydown', this.keydownHandler.bindAsEventListener(this));
+        document.observe('change', this.changeHandler.bindAsEventListener(this));
+        document.observe('dblclick', this.dblclickHandler.bindAsEventListener(this));
+        Event.observe(window, 'resize', this.onResize.bind(this));
+
+        /* Limit to folders sidebar only. */
+        $('foldersSidebar').observe('mouseover', this.mouseoverHandler.bindAsEventListener(this));
+
+        /* Show page now. */
+        $('sidebar').setStyle({ width: DIMP.conf.sidebar_width });
+        $('dimpLoading').hide();
+        $('dimpPage').show();
+
+        /* Create splitbar for sidebar. */
+        this.splitbar = new Element('DIV', { className: 'splitBarVertSidebar' }).setStyle({ height: document.viewport.getHeight() + 'px', left: $('sidebar').clientWidth + 'px' });
+        $('sidebar').insert({ after: this.splitbar });
+        new Drag(this.splitbar, {
+            constraint: 'horizontal',
+            ghosting: true,
+            nodrop: true
+        });
+
+        $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + this.splitbar.clientWidth) + 'px' });
+
+        /* Init quicksearch. These needs to occur before loading the message
+         * list since it may be disabled if we are in a search mailbox. */
+        if ($('qsearch')) {
+            $('qsearch_input').observe('blur', this._quicksearchOnBlur.bind(this));
+            DimpCore.addContextMenu({
+                id: 'qsearch_icon',
+                left: true,
+                offset: 'qsearch',
+                type: 'qsearchopts'
+            });
+            DimpCore.addContextMenu({
+                id: 'qsearch_icon',
+                left: false,
+                offset: 'qsearch',
+                type: 'qsearchopts'
+            });
+            DM.addSubMenu('ctx_qsearchopts_by', 'ctx_qsearchby');
+            DM.addSubMenu('ctx_qsearchopts_filter', 'ctx_flag');
+            DM.addSubMenu('ctx_qsearchopts_filternot', 'ctx_flag');
+        }
+
+        /* Store these text strings for updating purposes. */
+        DIMP.text.getmail = $('checkmaillink').down('A').innerHTML;
+        DIMP.text.refresh = $('refreshlink').down('A').innerHTML;
+        DIMP.text.showalog = $('alertsloglink').down('A').innerHTML;
+
+        /* Initialize the starting page. */
+        tmp = location.hash;
+        if (!tmp.empty() && tmp.startsWith('#')) {
+            tmp = (tmp.length == 1) ? "" : tmp.substring(1);
+        }
+
+        if (!tmp.empty()) {
+            this.go(decodeURIComponent(tmp));
+        } else if (DIMP.conf.login_view == 'inbox') {
+            this.go('folder:INBOX');
+        } else {
+            this.go('portal');
+            this.loadMailbox('INBOX', { background: true });
+        }
+
+        /* Create the folder list. Any pending notifications will be caught
+         * via the return from this call. */
+        this._listFolders({ initial: 1, mboxes: this.folder} );
+
+        this._setQsearchText(true);
+
+        /* Add popdown menus. Check for disabled compose at the same time. */
+        DimpCore.addPopdown('button_other', 'otheractions', true);
+        DimpCore.addPopdown('folderopts_link', 'folderopts', true);
+
+        DM.addSubMenu('ctx_message_reply', 'ctx_reply');
+        DM.addSubMenu('ctx_message_forward', 'ctx_forward');
+        [ 'ctx_message_', 'oa_' ].each(function(i) {
+            if ($(i + 'setflag')) {
+                DM.addSubMenu(i + 'setflag', 'ctx_flag');
+                DM.addSubMenu(i + 'unsetflag', 'ctx_flag');
+            }
+        });
+        DM.addSubMenu('ctx_folder_setflag', 'ctx_folder_flag');
+
+        if (DIMP.conf.disable_compose) {
+            $('button_reply', 'button_forward').compact().invoke('up', 'SPAN').concat($('button_compose', 'composelink', 'ctx_contacts_new')).compact().invoke('remove');
+        } else {
+            DimpCore.addPopdown('button_reply', 'reply', false, true);
+            DimpCore.addPopdown('button_forward', 'forward', false, true);
+        }
+
+        DimpCore.addContextMenu({
+            id: 'msglistHeader',
+            type: 'mboxsort'
+        });
+
+        new Drop('dropbase', this._folderDropConfig);
+
+        if (DIMP.conf.toggle_pref) {
+            this._toggleHeaders($('th_expand'));
+        }
+
+        /* Remove unavailable menu items. */
+        if (!$('GrowlerLog')) {
+            $('alertsloglink').remove();
+        }
+
+        /* Check for new mail. */
+        this.setPoll();
+    },
+
+    /* Resize function. */
+    onResize: function()
+    {
+        if (this.resize) {
+            clearTimeout(this.resize);
+        }
+
+        this.resize = this._onResize.bind(this).delay(0.1);
+    },
+
+    _onResize: function()
+    {
+        this._sizeFolderlist();
+        this.splitbar.setStyle({ height: document.viewport.getHeight() + 'px' });
+    },
+
+    /* Extend AJAX exception handling. */
+    onAjaxException: function(parentfunc, r, e)
+    {
+        /* Make sure loading images are closed. */
+        this.loadingImg('msg', false);
+        this.loadingImg('viewport', false);
+        DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_error } ]);
+        parentfunc(r, e);
+    }
+
+};
+
+/* Need to add after DimpBase is defined. */
+DimpBase._msgDragConfig = {
+    classname: 'msgdrag',
+    scroll: 'normalfolders',
+    threshold: 5,
+    caption: DimpBase.dragCaption.bind(DimpBase)
+};
+
+DimpBase._folderDragConfig = {
+    classname: 'folderdrag',
+    ghosting: true,
+    offset: { x: 15, y: 0 },
+    scroll: 'normalfolders',
+    threshold: 5
+};
+
+DimpBase._folderDropConfig = {
+    caption: function(drop, drag, e) {
+        var m,
+            d = drag.retrieve('l'),
+            ftype = drop.retrieve('ftype'),
+            l = drop.retrieve('l');
+
+        if (drop == $('dropbase')) {
+            return DIMP.text.moveto.sub('%s', d).sub('%s', DIMP.text.baselevel);
+        }
+
+        switch (e.type) {
+        case 'mousemove':
+            m = (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto;
+            break;
+
+        case 'keydown':
+            /* Can't use ctrlKey here since different browsers handle the
+             * ctrlKey in different ways when it comes to firing keyboard
+             * events. */
+            m = (e.keyCode == 17) ? DIMP.text.copyto : DIMP.text.moveto;
+            break;
+
+        case 'keyup':
+            m = (e.keyCode == 17)
+                ? DIMP.text.moveto
+                : (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto;
+            break;
+        }
+
+        if (drag.hasClassName('folder')) {
+            return (ftype != 'special' && !DimpBase.isSubfolder(drag, drop)) ? m.sub('%s', d).sub('%s', l) : '';
+        }
+
+        return ftype != 'container' ? m.sub('%s', DimpBase.dragCaption()).sub('%s', l) : '';
+    },
+    keypress: true
+};
+
+/* Drag/drop listeners. */
+document.observe('DragDrop2:drag', DimpBase.onDrag.bindAsEventListener(DimpBase));
+document.observe('DragDrop2:drop', DimpBase.folderDropHandler.bindAsEventListener(DimpBase));
+document.observe('DragDrop2:end', DimpBase.onDragEnd.bindAsEventListener(DimpBase));
+document.observe('DragDrop2:mousedown', DimpBase.onDragMouseDown.bindAsEventListener(DimpBase));
+document.observe('DragDrop2:mouseup', DimpBase.onDragMouseUp.bindAsEventListener(DimpBase));
+
+/* Route AJAX responses through ViewPort. */
+DimpCore.onDoActionComplete = function(r) {
+    DimpBase.deleteCallback(r);
+    if (DimpBase.viewport) {
+        DimpBase.viewport.parseJSONResponse(r);
+    }
+    DimpBase.pollCallback(r);
+};
+
+/* Click handler. */
+DimpCore.clickHandler = DimpCore.clickHandler.wrap(DimpBase.clickHandler.bind(DimpBase));
+
+/* ContextSensitive handlers. */
+DimpCore.contextOnClick = DimpCore.contextOnClick.wrap(DimpBase.contextOnClick.bind(DimpBase));
+DimpCore.contextOnShow = DimpCore.contextOnShow.wrap(DimpBase.contextOnShow.bind(DimpBase));
+
+/* Extend AJAX exception handling. */
+DimpCore.doActionOpts.onException = DimpCore.doActionOpts.onException.wrap(DimpBase.onAjaxException.bind(DimpBase));
+
+/* Initialize onload handler. */
+document.observe('dom:loaded', DimpBase.onDomLoad.bind(DimpBase));
diff --git a/imp/js/dimpcore.js b/imp/js/dimpcore.js
new file mode 100644 (file)
index 0000000..369aa0e
--- /dev/null
@@ -0,0 +1,595 @@
+/**
+ * dimpcore.js - Dimp UI application logic.
+ *
+ * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+/* DimpCore object. */
+var DimpCore = {
+    // Vars used and defaulting to null/false:
+    //   DMenu, Growler, inAjaxCallback, is_init, is_logout
+    //   onDoActionComplete
+    alarms: {},
+    growler_log: true,
+    server_error: 0,
+
+    doActionOpts: {
+        onException: function(r, e) { DimpCore.debug('onException', e); },
+        onFailure: function(t, o) { DimpCore.debug('onFailure', t); },
+        evalJS: false,
+        evalJSON: true
+    },
+
+    debug: function(label, e)
+    {
+        if (!this.is_logout && window.console && window.console.error) {
+            window.console.error(label, Prototype.Browser.Gecko ? e : $H(e).inspect());
+        }
+    },
+
+    // Convert object to an IMP UID Range string. See IMP::toRangeString()
+    // ob = (object) mailbox name as keys, values are array of uids.
+    toRangeString: function(ob)
+    {
+        var str = '';
+
+        $H(ob).each(function(o) {
+            if (!o.value.size()) {
+                return;
+            }
+
+            var u = o.value.numericSort(),
+                first = u.shift(),
+                last = first,
+                out = [];
+
+            u.each(function(k) {
+                if (last + 1 == k) {
+                    last = k;
+                } else {
+                    out.push(first + (last == first ? '' : (':' + last)));
+                    first = last = k;
+                }
+            });
+            out.push(first + (last == first ? '' : (':' + last)));
+            str += '{' + o.key.length + '}' + o.key + out.join(',');
+        });
+
+        return str;
+    },
+
+    // Parses an IMP UID Range string. See IMP::parseRangeString()
+    // str = (string) An IMP UID range string.
+    parseRangeString: function(str)
+    {
+        var count, end, i, mbox, uidstr,
+            mlist = {},
+            uids = [];
+        str = str.strip();
+
+        while (!str.blank()) {
+            if (!str.startsWith('{')) {
+                break;
+            }
+            i = str.indexOf('}');
+            count = Number(str.substr(1, i - 1));
+            mbox = str.substr(i + 1, count);
+            i += count + 1;
+            end = str.indexOf('{', i);
+            if (end == -1) {
+                uidstr = str.substr(i);
+                str = '';
+            } else {
+                uidstr = str.substr(i, end - i);
+                str = str.substr(end);
+            }
+
+            uidstr.split(',').each(function(e) {
+                var r = e.split(':');
+                if (r.size() == 1) {
+                    uids.push(Number(e));
+                } else {
+                    uids = uids.concat($A($R(Number(r[0]), Number(r[1]))));
+                }
+            });
+
+            mlist[mbox] = uids;
+        }
+
+        return mlist;
+    },
+
+    // 'opts' -> ajaxopts, callback, uids
+    doAction: function(action, params, opts)
+    {
+        params = $H(params);
+        opts = opts || {};
+
+        var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {});
+
+        if (opts.uids) {
+            if (opts.uids.viewport_selection) {
+                opts.uids = this.selectionToRange(opts.uids);
+            }
+            params.set('uid', this.toRangeString(opts.uids));
+        }
+
+        ajaxopts.parameters = this.addRequestParams(params);
+        ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this);
+
+        new Ajax.Request(DIMP.conf.URI_AJAX + action, ajaxopts);
+    },
+
+    // 'opts' -> ajaxopts, callback
+    submitForm: function(form, opts)
+    {
+        opts = opts || {};
+        var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {});
+        ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this);
+        $(form).request(ajaxopts);
+    },
+
+    selectionToRange: function(s)
+    {
+        var b = s.getBuffer(),
+            tmp = {};
+
+        if (b.getMetaData('search')) {
+            s.get('uid').each(function(r) {
+                var parts = r.split(DIMP.conf.IDX_SEP);
+                if (tmp[parts[0]]) {
+                    tmp[parts[0]].push(parts[1]);
+                } else {
+                    tmp[parts[0]] = [ parts[1] ];
+                }
+            });
+        } else {
+            tmp[b.getView()] = s.get('uid');
+        }
+
+        return tmp;
+    },
+
+    // params - (Hash)
+    addRequestParams: function(params)
+    {
+        var p = params.clone();
+
+        if (DIMP.conf.SESSION_ID) {
+            p.update(DIMP.conf.SESSION_ID.toQueryParams());
+        }
+
+        return p;
+    },
+
+    doActionComplete: function(request, callback)
+    {
+        this.inAjaxCallback = true;
+
+        if (!request.responseJSON) {
+            if (++this.server_error == 3) {
+                this.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_timeout } ]);
+            }
+            this.inAjaxCallback = false;
+            return;
+        }
+
+        var r = request.responseJSON;
+
+        if (!r.msgs) {
+            r.msgs = [];
+        }
+
+        if (r.response && Object.isFunction(callback)) {
+            try {
+                callback(r);
+            } catch (e) {
+                this.debug('doActionComplete', e);
+            }
+        }
+
+        if (this.server_error >= 3) {
+            r.msgs.push({ type: 'horde.success', message: DIMP.text.ajax_recover });
+        }
+        this.server_error = 0;
+
+        this.showNotifications(r.msgs);
+
+        if (r.response && this.onDoActionComplete) {
+            this.onDoActionComplete(r.response);
+        }
+
+        this.inAjaxCallback = false;
+    },
+
+    setTitle: function(title)
+    {
+        document.title = DIMP.conf.name + ' :: ' + title;
+    },
+
+    showNotifications: function(msgs)
+    {
+        if (!msgs.size() || this.is_logout) {
+            return;
+        }
+
+        msgs.find(function(m) {
+            switch (m.type) {
+            case 'horde.ajaxtimeout':
+                this.logout(m.message);
+                return true;
+
+            case 'horde.alarm':
+                if (!this.alarms[m.flags.alarm.id]) {
+                    this.Growler.growl(m.flags.alarm.title + ': ' + m.flags.alarm.text, {
+                        className: 'horde-alarm',
+                        sticky: 1,
+                        log: 1
+                    });
+                    this.alarms[m.flags.alarm.id] = 1;
+                }
+                break;
+
+            case 'horde.error':
+            case 'horde.message':
+            case 'horde.success':
+            case 'horde.warning':
+                this.Growler.growl(m.message, {
+                    className: m.type.replace('.', '-'),
+                    life: (m.type == 'horde.error' ? 12 : 8),
+                    log: 1
+                });
+                break;
+
+            case 'imp.reply':
+            case 'imp.forward':
+            case 'imp.redirect':
+                this.Growler.growl(m.message, {
+                    className: m.type.replace('.', '-'),
+                    life: 8
+                });
+                break;
+            }
+        }, this);
+    },
+
+    compose: function(type, args)
+    {
+        var url = DIMP.conf.URI_COMPOSE;
+        args = args || {};
+        if (type) {
+            args.type = type;
+        }
+        this.popupWindow(this.addURLParam(url, args), 'compose' + new Date().getTime());
+    },
+
+    popupWindow: function(url, name, onload)
+    {
+        var opts = {
+            height: DIMP.conf.popup_height,
+            name: name.gsub(/\W/, '_'),
+            noalert: true,
+            onload: onload,
+            url: url,
+            width: DIMP.conf.popup_width
+        };
+
+        if (!Horde.popup(opts)) {
+            this.showNotifications([ { type: 'horde.warning', message: DIMP.text.popup_block } ]);
+        }
+    },
+
+    closePopup: function()
+    {
+        // Mozilla bug/feature: it will not close a browser window
+        // automatically if there is code remaining to be performed (or, at
+        // least, not here) unless the mouse is moved or a keyboard event
+        // is triggered after the callback is complete. (As of FF 2.0.0.3 and
+        // 1.5.0.11).  So wait for the callback to complete before attempting
+        // to close the window.
+        if (this.inAjaxCallback) {
+            this.closePopup.bind(this).defer();
+        } else {
+            window.close();
+        }
+    },
+
+    logout: function(url)
+    {
+        this.is_logout = true;
+        this.redirect(url || (DIMP.conf.URI_AJAX + 'logOut'));
+    },
+
+    redirect: function(url, force)
+    {
+        var ptr = parent.frames.horde_main ? parent : window;
+
+        ptr.location.assign(this.addURLParam(url));
+
+        // Catch browsers that don't redirect on assign().
+        if (force && !Prototype.Browser.WebKit) {
+            (function() { ptr.location.reload(); }).delay(0.5);
+        }
+    },
+
+    loadingImg: function(elt, id, show)
+    {
+        elt = $(elt);
+
+        if (show) {
+            elt.clonePosition(id, { setHeight: false, setLeft: false, setWidth: false }).show();
+        } else {
+            elt.fade({ duration: 0.2 });
+        }
+    },
+
+    toggleButtons: function(elts, disable)
+    {
+        elts.each(function(b) {
+            var tmp;
+            [ b.up() ].invoke(disable ? 'addClassName' : 'removeClassName', 'disabled');
+            if (this.DMenu &&
+                (tmp = b.next('.popdown'))) {
+                this.DMenu.disable(tmp.identify(), true, disable);
+            }
+        }, this);
+    },
+
+    // p = (Element) Parent element
+    // t = (string) Context menu type
+    // trigger = (boolean) Trigger popdown on button click?
+    // d = (boolean) Disabled?
+    addPopdown: function(p, t, trigger, d)
+    {
+        var elt = new Element('SPAN', { className: 'iconImg popdownImg popdown' });
+        p = $(p);
+
+        p.insert({ after: elt });
+
+        if (trigger) {
+            this.addContextMenu({
+                disable: d,
+                id: p.identify(),
+                left: true,
+                offset: p.up(),
+                type: t
+            });
+        }
+
+        this.addContextMenu({
+            disable: d,
+            id: elt.identify(),
+            left: true,
+            offset: elt.up(),
+            type: t
+        });
+
+        return elt;
+    },
+
+    addContextMenu: function(p)
+    {
+        if (this.DMenu) {
+            this.DMenu.addElement(p.id, 'ctx_' + p.type, p);
+        }
+    },
+
+    /* Add dropdown menus to addresses. */
+    buildAddressLinks: function(alist, elt)
+    {
+        var base, tmp,
+            cnt = alist.size();
+
+        if (cnt > 15) {
+            tmp = $('largeaddrspan').cloneNode(true).writeAttribute('id', 'largeaddrspan_active');
+            elt.insert(tmp);
+            base = tmp.down('.dispaddrlist');
+            tmp = tmp.down('.largeaddrlist');
+            tmp.setText(tmp.getText().replace('%d', cnt));
+        } else {
+            base = elt;
+        }
+
+        alist.each(function(o, i) {
+            var a;
+            if (o.raw) {
+                a = o.raw;
+            } else {
+                a = new Element('A', { className: 'address' }).store({ personal: o.personal, email: o.inner, address: (o.personal ? (o.personal + ' <' + o.inner + '>') : o.inner) });
+                if (o.personal) {
+                    a.writeAttribute({ title: o.inner }).insert(o.personal.escapeHTML());
+                } else {
+                    a.insert(o.inner.escapeHTML());
+                }
+                this.DMenu.addElement(a.identify(), 'ctx_contacts', { offset: a, left: true });
+            }
+            base.insert(a);
+            if (i + 1 != cnt) {
+                base.insert(', ');
+            }
+        }, this);
+
+        return elt;
+    },
+
+    /* Add message log info to message view. */
+    updateMsgLog: function(log)
+    {
+        var tmp = '';
+        log.each(function(entry) {
+            tmp += '<li><span class="iconImg imp-' + entry.t + '"></span>' + entry.m + '</li>';
+        });
+        $('msgloglist').down('UL').update(tmp);
+    },
+
+    /* Removes event handlers from address links. */
+    removeAddressLinks: function(id)
+    {
+        id.select('.address').each(function(elt) {
+            this.DMenu.removeElement(elt.identify());
+        }, this);
+    },
+
+    addURLParam: function(url, params)
+    {
+        var q = url.indexOf('?');
+        params = $H(params);
+
+        if (DIMP.conf.SESSION_ID) {
+            params.update(DIMP.conf.SESSION_ID.toQueryParams());
+        }
+
+        if (q != -1) {
+            params.update(url.toQueryParams());
+            url = url.substring(0, q);
+        }
+
+        return params.size() ? (url + '?' + params.toQueryString()) : url;
+    },
+
+    reloadMessage: function(params)
+    {
+        if (typeof DimpFullmessage != 'undefined') {
+            window.location = this.addURLParam(document.location.href, params);
+        } else {
+            DimpBase.loadPreview(null, params);
+        }
+    },
+
+    /* Mouse click handler. */
+    clickHandler: function(e)
+    {
+        if (e.isRightClick()) {
+            return;
+        }
+
+        var elt = e.element(), id, tmp;
+
+        while (Object.isElement(elt)) {
+            id = elt.readAttribute('id');
+
+            switch (id) {
+            case 'largeaddrspan_active':
+                tmp = elt.down();
+                if (!tmp.next().visible() ||
+                    e.element().hasClassName('largeaddrlist')) {
+                    [ tmp.down(), tmp.down(1), tmp.next() ].invoke('toggle');
+                }
+                break;
+
+            default:
+                // CSS class based matching
+                if (elt.hasClassName('unblockImageLink')) {
+                    IMP.unblockImages(e);
+                } else if (elt.hasClassName('toggleQuoteShow')) {
+                    [ elt, elt.next() ].invoke('toggle');
+                    elt.next(1).blindDown({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } });
+                } else if (elt.hasClassName('toggleQuoteHide')) {
+                    [ elt, elt.previous() ].invoke('toggle');
+                    elt.next().blindUp({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } });
+                } else if (elt.hasClassName('pgpVerifyMsg')) {
+                    elt.replace(DIMP.text.verify);
+                    DimpCore.reloadMessage({ pgp_verify_msg: 1 });
+                    e.stop();
+                } else if (elt.hasClassName('smimeVerifyMsg')) {
+                    elt.replace(DIMP.text.verify);
+                    DimpCore.reloadMessage({ smime_verify_msg: 1 });
+                    e.stop();
+                }
+                break;
+            }
+
+            elt = elt.up();
+        }
+    },
+
+    contextOnShow: function(e)
+    {
+        var tmp;
+
+        switch (e.memo) {
+        case 'ctx_contacts':
+            tmp = $(e.memo).down('DIV.contactAddr');
+            if (tmp) {
+                tmp.next().remove();
+                tmp.remove();
+            }
+
+            // Add e-mail info to context menu if personal name is shown on
+            // page.
+            if (e.element().retrieve('personal')) {
+                $(e.memo)
+                    .insert({ top: new Element('DIV', { className: 'sep' }) })
+                    .insert({ top: new Element('DIV', { className: 'contactAddr' }).insert(e.element().retrieve('email').escapeHTML()) });
+            }
+            break;
+        }
+    },
+
+    contextOnClick: function(e)
+    {
+        var baseelt = e.element();
+
+        switch (e.memo.elt.readAttribute('id')) {
+        case 'ctx_contacts_new':
+            this.compose('new', { to: baseelt.retrieve('address') });
+            break;
+
+        case 'ctx_contacts_add':
+            this.doAction('addContact', { name: baseelt.retrieve('personal'), email: baseelt.retrieve('email') }, {}, true);
+            break;
+        }
+    },
+
+    /* DIMP initialization function. */
+    init: function()
+    {
+        if (this.is_init) {
+            return;
+        }
+        this.is_init = true;
+
+        if (typeof ContextSensitive != 'undefined') {
+            this.DMenu = new ContextSensitive();
+            document.observe('ContextSensitive:click', this.contextOnClick.bindAsEventListener(this));
+            document.observe('ContextSensitive:show', this.contextOnShow.bindAsEventListener(this));
+        }
+
+        /* Add Growler notification handler. */
+        this.Growler = new Growler({
+            location: 'br',
+            log: this.growler_log,
+            noalerts: DIMP.text.noalerts
+        });
+
+        /* Add click handler. */
+        document.observe('click', DimpCore.clickHandler.bindAsEventListener(DimpCore));
+
+        /* Catch dialog actions. */
+        document.observe('IMPDialog:success', function(e) {
+            switch (e.memo) {
+            case 'pgpPersonal':
+            case 'pgpSymmetric':
+            case 'smimePersonal':
+                IMPDialog.noreload = true;
+                this.reloadMessage({});
+                break;
+            }
+        }.bindAsEventListener(this));
+
+        /* Determine base window. Need a try/catch block here since, if the
+         * page was loaded by an opener out of this current domain, this will
+         * throw an exception. */
+        try {
+            if (parent.opener &&
+                parent.opener.location.host == window.location.host &&
+                parent.opener.DimpCore) {
+                DIMP.baseWindow = parent.opener.DIMP.baseWindow || parent.opener;
+            }
+        } catch (e) {}
+    }
+
+};
diff --git a/imp/js/viewport.js b/imp/js/viewport.js
new file mode 100644 (file)
index 0000000..7fb8cde
--- /dev/null
@@ -0,0 +1,1846 @@
+/**
+ * viewport.js - Code to create a viewport window, with optional split pane
+ * functionality.
+ *
+ * Usage:
+ * ======
+ * var viewport = new ViewPort({ options });
+ *
+ * Required options:
+ * -----------------
+ * ajax_url: (string) The URL to send the viewport requests to.
+ *           This URL should return its response in an object named
+ *           'ViewPort' (other information can be returned in the response and
+ *           will be ignored by this class).
+ * container: (Element/string) A DOM element/ID of the container that holds
+ *            the viewport. This element should be empty and have no children.
+ * onContent: (function) A function that takes 2 arguments - the data object
+ *            for the row and a string indicating the current pane_mode.
+ *
+ *            This function MUST return the HTML representation of the row.
+ *
+ *            This representation MUST include both the DOM ID (stored in
+ *            the VP_domid data entry) and the CSS class name (stored as an
+ *            array in the VP_bg data entry) in the outermost element.
+ *
+ *            Selected rows will contain the classname 'vpRowSelected'.
+ *
+ *
+ * Optional options:
+ * -----------------
+ * ajax_opts: (object) Any additional options to pass to the Ajax.Request
+ *            object when sending an AJAX message.
+ * buffer_pages: (integer) The number of viewable pages to send to the browser
+ *               per server access when listing rows.
+ * empty_msg: (string) A string to display when the view is empty. Inserted in
+ *            a SPAN element with class 'vpEmpty'.
+ * limit_factor: (integer) When browsing through a list, if a user comes
+ *               within this percentage of the end of the current cached
+ *               viewport, send a background request to the server to retrieve
+ *               the next slice.
+ * list_class: (string) The CSS class to use for the list container.
+ * lookbehind: (integer) What percentage of the received buffer should be
+ *             used to download rows before the given row number?
+ * onAjaxFailure: (function) Callback function that handles a failure response
+ *                from an AJAX request.
+ *                params: (XMLHttpRequest object)
+ *                        (mixed) Result of evaluating the X-JSON response
+ *                        header, if any (can be null).
+ *                return: NONE
+ * onAjaxRequest: (function) Callback function that allows additional
+ *                parameters to be added to the outgoing AJAX request.
+                  params: (string) The current view.
+                  return: (Hash) Parameters to add to the outgoing request.
+ * onAjaxResponse: (function) Callback function that allows user-defined code
+ *                 to additionally process the AJAX return data.
+ *                params: (XMLHttpRequest object)
+ *                        (mixed) Result of evaluating the X-JSON response
+ *                        header, if any (can be null).
+ *                return: NONE
+ * onCachedList: (function) Callback function that allows the cache ID string
+ *               to be dynamically generated.
+                 params: (string) The current view.
+                 return: (string) The cache ID string to use.
+ * onContentOffset: (function) Callback function that alters the starting
+ *                  offset of the content about to be rendered.
+ *                  params: (integer) The current offset.
+ *                  return: (integer) The altered offset.
+ * onSlide: (function) Callback function that is triggered when the
+ *          viewport slider bar is moved.
+ *          params: NONE
+ *          return: NONE
+ * page_size: (integer) Default page size to view on load. Only used if
+ *            pane_mode is 'horiz'.
+ * pane_data: (Element/string) A DOM element/ID of the container to hold
+ *            the split pane data. This element will be moved inside of the
+ *            container element.
+ * pane_mode: (string) The split pane mode to show on load? Either empty,
+ *            'horiz', or 'vert'.
+ * pane_width: (integer) The default pane width to use on load. Only used if
+ *             pane_mode is 'vert'.
+ * split_bar_class: (object) The CSS class(es) to use for the split bar.
+ *                  Takes two properties: 'horiz' and 'vert'.
+ * wait: (integer) How long, in seconds, to wait before displaying an
+ *       informational message to users that the list is still being
+ *       built.
+ *
+ *
+ * Custom events:
+ * --------------
+ * Custom events are triggered on the container element. The parameters given
+ * below are available through the 'memo' property of the Event object.
+ *
+ * ViewPort:add
+ *   Fired when a row has been added to the screen.
+ *   params: (Element) The viewport row being added.
+ *
+ * ViewPort:cacheUpdate
+ *   Fired when the internal cached data of a view is changed.
+ *   params: (string) View which is being updated.
+ *
+ * ViewPort:clear
+ *   Fired when a row is being removed from the screen.
+ *   params: (Element) The viewport row being removed.
+ *
+ * ViewPort:contentComplete
+ *   Fired when the view has changed and all viewport rows have been added.
+ *   params: NONE
+ *
+ * ViewPort:deselect
+ *   Fired when rows are deselected.
+ *   params: (object) opts = (object) Boolean options [right]
+ *                    vs = (ViewPort_Selection) A ViewPort_Selection object.
+ *
+ * ViewPort:endFetch
+ *   Fired when a fetch AJAX response is completed.
+ *   params: (string) Current view.
+ *
+ * ViewPort:fetch
+ *   Fired when a non-background AJAX response is sent.
+ *   params: (string) Current view.
+ *
+ * ViewPort:select
+ *   Fired when rows are selected.
+ *   params: (object) opts = (object) Boolean options [delay, right]
+ *                    vs = (ViewPort_Selection) A ViewPort_Selection object.
+ *
+ * ViewPort:splitBarChange
+ *   Fired when the splitbar is moved.
+ *   params: (string) The current pane mode ('horiz' or 'vert').
+ *
+ * ViewPort:splitBarEnd
+ *   Fired when the splitbar is released.
+ *   params: (string) The current pane mode ('horiz' or 'vert').
+ *
+ * ViewPort:splitBarStart
+ *   Fired when the splitbar is initially clicked.
+ *   params: (string) The current pane mode ('horiz' or 'vert').
+ *
+ * ViewPort:wait
+ *   Fired if viewport_wait seconds have passed since request was sent.
+ *   params: (string) Current view.
+ *
+ *
+ * Outgoing AJAX request has the following params:
+ * -----------------------------------------------
+ * For ALL requests:
+ *   cache: (string) The list of uids cached on the browser.
+ *   cacheid: (string) A unique string that changes whenever the viewport
+ *            list changes.
+ *   initial: (integer) This is the initial browser request for this view.
+ *   requestid: (integer) A unique identifier for this AJAX request.
+ *   view: (string) The view of the request.
+ *
+ * For a row request:
+ *   slice: (string) The list of rows to retrieve from the server.
+ *          In the format: [first_row]:[last_row]
+ *
+ * For a search request:
+ *   after: (integer) The number of rows to return after the selected row.
+ *   before: (integer) The number of rows to return before the selected row.
+ *   search: (JSON object) The search query.
+ *
+ * For a rangeslice request:
+ *   rangeslice: (integer) If present, indicates that slice is a rangeslice
+ *               request.
+ *   slice: (string) The list of rows to retrieve from the server.
+ *          In the format: [first_row]:[last_row]
+ *
+ *
+ * Incoming AJAX response has the following params:
+ * ------------------------------------------------
+ * cacheid: (string) A unique string that changes whenever the viewport
+ *          list changes.
+ * data: (object) Data for each entry that is passed to the template to create
+ *       the viewable rows. Keys are a unique ID (see also the 'rowlist'
+ *       entry). Values are the data objects. Internal keys for these data
+ *       objects must NOT begin with the string 'VP_'.
+ * disappear: (array) If update is set, this is the list of unique IDs that
+ *            have been cached by the browser but no longer appear on the
+ *            server.
+ * label: (string) [REQUIRED when initial is true] The label to use for the
+ *        view.
+ * metadata [optional]: (object) Metadata for the view. Entries in buffer are
+ *                      updated with these entries (unless resetmd is set).
+ * rangelist [optional]: (object) The list of unique IDs -> rownumbers that
+ *                       correspond the the given request. Only returned for
+ *                       a rangeslice request.
+ * requestid: (string) The request ID sent in the outgoing AJAX request.
+ * reset [optional]: (integer) If set, purges all cached data.
+ * resetmd [optional]: (integer) If set, purges all user metadata.
+ * rowlist: (object) A mapping of unique IDs (keys) to the row numbers
+ *          (values). Row numbers start at 1.
+ * rownum [optional]: (integer) The row number to position screen on.
+ * totalrows: (integer) Total number of rows in the view.
+ * update [optional]: (integer) If set, update the rowlist instead of
+ *                    overwriting it.
+ * updatecacheid [optional]: (string) If set, simply update the cacheid with
+ *                           the new value. Indicates that the browser
+ *                           contains the up-to-date version of the cache.
+ * view: (string) The view ID of the request.
+ *
+ *
+ * Data entries:
+ * -------------
+ * In addition to the data provided from the server, the following
+ * dynamically created entries are also available:
+ *   VP_domid: (string) The DOM ID of the row.
+ *   VP_id: (string) The unique ID used to store the data entry.
+ *   VP_rownum: (integer) The row number of the row.
+ *
+ *
+ * Scroll bars use ars styled using these CSS class names:
+ * -------------------------------------------------------
+ * vpScroll - The scroll bar container.
+ * vpScrollUp - The UP arrow.
+ * vpScrollCursor - The cursor used to slide within the bounds.
+ * vpScrollDown - The DOWN arrow.
+ *
+ *
+ * Requires prototypejs 1.6+, scriptaculous 1.8+ (effects.js only), and
+ * Horde's dragdrop2.js and slider2.js.
+ *
+ * Copyright 2005-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+/**
+ * ViewPort
+ */
+var ViewPort = Class.create({
+
+    initialize: function(opts)
+    {
+        this.opts = Object.extend({
+            buffer_pages: 10,
+            limit_factor: 35,
+            lookbehind: 40,
+            split_bar_class: {}
+        }, opts);
+
+        this.opts.container = $(opts.container);
+        this.opts.pane_data = $(opts.pane_data);
+
+        this.opts.content = new Element('DIV', { className: opts.list_class }).setStyle({ float: 'left', overflow: 'hidden' });
+        this.opts.container.insert(this.opts.content);
+
+        this.scroller = new ViewPort_Scroller(this);
+
+        this.split_pane = {
+            curr: null,
+            currbar: null,
+            horiz: {
+                loc: opts.page_size
+            },
+            init: false,
+            spacer: null,
+            vert: {
+                width: opts.pane_width
+            }
+        };
+        this.views = {};
+
+        this.pane_mode = opts.pane_mode;
+
+        this.isbusy = this.page_size = null;
+        this.request_num = 1;
+
+        // Init empty string now.
+        this.empty_msg = new Element('SPAN', { className: 'vpEmpty' }).insert(opts.empty_msg);
+
+        // Set up AJAX response function.
+        this.ajax_response = this.opts.onAjaxResponse || this._ajaxRequestComplete.bind(this);
+
+        Event.observe(window, 'resize', this.onResize.bind(this));
+    },
+
+    // view = (string) ID of view.
+    // opts = (object) background: (boolean) Load view in background?
+    //                 search: (object) Search parameters
+    loadView: function(view, opts)
+    {
+        var buffer, curr, ps,
+            f_opts = {},
+            init = true;
+
+        this._clearWait();
+
+        // Need a page size before we can continue - this is what determines
+        // the slice size to request from the server.
+        if (this.page_size === null) {
+            ps = this.getPageSize(this.pane_mode ? 'default' : 'max');
+            if (isNaN(ps)) {
+                return this.loadView.bind(this, view, opts).defer();
+            }
+            this.page_size = ps;
+        }
+
+        if (this.view) {
+            if (!opts.background && (view != this.view)) {
+                // Need to store current buffer to save current offset
+                buffer = this._getBuffer();
+                buffer.setMetaData({ offset: this.currentOffset() }, true);
+                this.views[this.view] = buffer;
+            }
+            init = false;
+        }
+
+        if (opts.background) {
+            f_opts = { background: true, view: view };
+        } else {
+            if (!this.view) {
+                this.onResize(true);
+            } else if (this.view != view) {
+                this.active_req = null;
+            }
+            this.view = view;
+        }
+
+        if (curr = this.views[view]) {
+            this._updateContent(curr.getMetaData('offset') || 0, f_opts);
+            if (!opts.background) {
+                this._ajaxRequest({ checkcache: 1 });
+            }
+            return;
+        }
+
+        if (!init) {
+            this.visibleRows().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
+            this.opts.content.update();
+            this.scroller.clear();
+        }
+
+        this.views[view] = buffer = this._getBuffer(view, true);
+
+        if (opts.search) {
+            f_opts.search = opts.search;
+        } else {
+            f_opts.offset = 0;
+        }
+
+        f_opts.initial = 1;
+
+        this._fetchBuffer(f_opts);
+    },
+
+    // view = ID of view
+    deleteView: function(view)
+    {
+        delete this.views[view];
+    },
+
+    // rownum = (integer) Row number
+    // opts = (Object) [noupdate, top] TODO
+    scrollTo: function(rownum, opts)
+    {
+        var s = this.scroller;
+        opts = opts || {};
+
+        s.noupdate = opts.noupdate;
+
+        switch (this.isVisible(rownum)) {
+        case -1:
+            s.moveScroll(rownum - 1);
+            break;
+
+        case 0:
+            if (opts.top) {
+                s.moveScroll(rownum - 1);
+            }
+            break;
+
+        case 1:
+            s.moveScroll(Math.min(rownum - 1, this.getMetaData('total_rows') - this.getPageSize()));
+            break;
+        }
+
+        s.noupdate = false;
+    },
+
+    // rownum = (integer) Row number
+    isVisible: function(rownum)
+    {
+        var offset = this.currentOffset();
+        return (rownum < offset + 1)
+            ? -1
+            : ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0);
+    },
+
+    // params = (object) Parameters to add to outgoing URL
+    reload: function(params)
+    {
+        this._fetchBuffer({
+            offset: this.currentOffset(),
+            params: $H(params),
+            purge: true
+        });
+    },
+
+    // vs = (Viewport_Selection) A Viewport_Selection object.
+    // opts = (object) TODO [noupdate, view]
+    remove: function(vs, opts)
+    {
+        if (!vs.size()) {
+            return;
+        }
+
+        if (this.isbusy) {
+            this.remove.bind(this, vs, opts).defer();
+            return;
+        }
+
+        this.isbusy = true;
+        opts = opts || {};
+
+        var args = { duration: 0.2, to: 0.01 },
+            visible = vs.get('div');
+
+        this.deselect(vs);
+
+        // If we have visible elements to remove, only call refresh after
+        // the last effect has finished.
+        if (visible.size()) {
+            // Set 'to' to a value slightly above 0 to prevent fade()
+            // from auto hiding.  Hiding is unnecessary, since we will be
+            // removing from the document shortly.
+            visible.slice(0, -1).invoke('fade', args);
+            args.afterFinish = this._removeids.bind(this, vs, opts);
+            visible.last().fade(args);
+        } else {
+            this._removeids(vs, opts);
+        }
+    },
+
+    // vs = (Viewport_Selection) A Viewport_Selection object.
+    // opts = (object) TODO [noupdate, view]
+    _removeids: function(vs, opts)
+    {
+        this._getBuffer(opts.view).setMetaData({ total_rows: this.getMetaData('total_rows', opts.view) - vs.size() }, true);
+
+        this._getBuffer().remove(vs.get('rownum'));
+        this.opts.container.fire('ViewPort:cacheUpdate', opts.view || this.view);
+
+        if (!opts.noupdate) {
+            this.requestContentRefresh(this.currentOffset());
+        }
+
+        this.isbusy = false;
+    },
+
+    // nowait = (boolean) If true, don't delay before resizing.
+    // size = (integer) The page size to use instead of auto-determining.
+    onResize: function(nowait, size)
+    {
+        if (!this.opts.content.visible()) {
+            return;
+        }
+
+        if (this.resizefunc) {
+            clearTimeout(this.resizefunc);
+        }
+
+        if (nowait) {
+            this._onResize(size);
+        } else {
+            this.resizefunc = this._onResize.bind(this, size).delay(0.1);
+        }
+    },
+
+    // size = (integer) The page size to use instead of auto-determining.
+    _onResize: function(size)
+    {
+        var h,
+            c = this.opts.content,
+            c_opts = {},
+            lh = this._getLineHeight(),
+            sp = this.split_pane;
+
+        if (size) {
+            this.page_size = size;
+        }
+
+        if (this.view && sp.curr != this.pane_mode) {
+            c_opts.updated = this.createSelection('div', this.visibleRows()).get('domid');
+        }
+
+        // Get split pane dimensions
+        switch (this.pane_mode) {
+        case 'horiz':
+            this._initSplitBar();
+
+            if (!size) {
+                this.page_size = (sp.horiz.loc && sp.horiz.loc > 0)
+                    ? Math.min(sp.horiz.loc, this.getPageSize('splitmax'))
+                    : this.getPageSize('default');
+            }
+            sp.horiz.loc = this.page_size;
+
+            if (sp.spacer) {
+                sp.spacer.hide();
+            }
+
+            h = lh * this.page_size;
+            c.setStyle({ height: h + 'px', width: '100%' });
+            sp.currbar.show();
+            this.opts.pane_data.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show();
+            break;
+
+        case 'vert':
+            this._initSplitBar();
+
+            if (!size) {
+                this.page_size = this.getPageSize('max');
+            }
+
+            if (!sp.vert.width) {
+                sp.vert.width = parseInt(this.opts.container.clientWidth * 0.35, 10);
+            }
+
+            if (sp.spacer) {
+                sp.spacer.hide();
+            }
+
+            h = lh * this.page_size;
+            c.setStyle({ height: h + 'px', width: sp.vert.width + 'px' });
+            sp.currbar.setStyle({ height: h + 'px' }).show();
+            this.opts.pane_data.setStyle({ height: h + 'px' }).show();
+            break;
+
+        default:
+            if (sp.curr) {
+                if (this.pane_mode == 'horiz') {
+                    sp.horiz.loc = this.page_size;
+                }
+                [ this.opts.pane_data, sp.currbar ].invoke('hide');
+                sp.curr = sp.currbar = null;
+            }
+
+            if (!size) {
+                this.page_size = this.getPageSize('max');
+            }
+
+            if (sp.spacer) {
+                sp.spacer.show();
+            } else {
+                sp.spacer = new Element('DIV').setStyle({ clear: 'left' });
+                this.opts.content.up().insert(sp.spacer);
+            }
+
+            c.setStyle({ height: (lh * this.page_size) + 'px', width: '100%' });
+            break;
+        }
+
+        if (this.view) {
+            this.requestContentRefresh(this.currentOffset(), c_opts);
+        }
+    },
+
+    // offset = (integer) TODO
+    // opts = (object) See _updateContent()
+    requestContentRefresh: function(offset, opts)
+    {
+        if (!this._updateContent(offset, opts)) {
+            return false;
+        }
+
+        var limit = this._getBuffer().isNearingLimit(offset);
+        if (limit) {
+            this._fetchBuffer({
+                background: true,
+                nearing: limit,
+                offset: offset
+            });
+        }
+
+        return true;
+    },
+
+    // opts = (object) The following parameters:
+    // One of the following is REQUIRED:
+    //   offset: (integer) Value of offset
+    //   search: (object) List of search keys/values
+    //
+    // OPTIONAL:
+    //   background: (boolean) Do fetch in background
+    //   callback: (function) A callback to run when the request is complete
+    //   initial: (boolean) Is this the initial access to this view?
+    //   nearing: (string) TODO [only used w/offset]
+    //   params: (object) Parameters to add to outgoing URL
+    //   purge: (boolean) If true, purge the current rowlist and rebuild.
+    //          Attempts to reuse the current data cache.
+    //   view: (string) The view to retrieve. Defaults to current view.
+    _fetchBuffer: function(opts)
+    {
+        if (this.isbusy) {
+            return this._fetchBuffer.bind(this, opts).defer();
+        }
+
+        this.isbusy = true;
+
+        var llist, lrows, rlist, tmp, type, value,
+            view = (opts.view || this.view),
+            b = this._getBuffer(view),
+            params = $H(opts.params),
+            r_id = this.request_num++;
+
+        // Only fire fetch event if we are loading in foreground.
+        if (!opts.background) {
+            this.opts.container.fire('ViewPort:fetch', view);
+        }
+
+        params.update({ requestid: r_id });
+
+        // Determine if we are querying via offset or a search query
+        if (opts.search || opts.initial || opts.purge) {
+            /* If this is an initial request, 'type' will be set correctly
+             * further down in the code. */
+            if (opts.search) {
+                type = 'search';
+                value = opts.search;
+                params.set('search', Object.toJSON(value));
+            }
+
+            if (opts.initial) {
+                params.set('initial', 1);
+            }
+
+            if (opts.purge) {
+                b.resetRowlist();
+            }
+
+            tmp = this._lookbehind();
+
+            params.update({
+                after: this.bufferSize() - tmp,
+                before: tmp
+            });
+        }
+
+        if (!opts.search) {
+            type = 'rownum';
+            value = opts.offset + 1;
+
+            // llist: keys - request_ids; vals - loading rownums
+            llist = b.getMetaData('llist') || $H();
+            lrows = llist.values().flatten();
+
+            b.setMetaData({ req_offset: opts.offset }, true);
+
+            /* If the current offset is part of a pending request, update
+             * the offset. */
+            if (lrows.size() &&
+                b.sliceLoaded(value, lrows)) {
+                /* One more hurdle. If we are loading in background, and now
+                 * we are in foreground, we need to search for the request
+                 * that contains the current rownum. For now, just use the
+                 * last request. */
+                if (!this.active_req && !opts.background) {
+                    this.active_req = llist.keys().numericSort().last();
+                }
+                this.isbusy = false;
+                return;
+            }
+
+            /* This gets the list of rows needed which do not already appear
+             * in the buffer. */
+            tmp = this._getSliceBounds(value, opts.nearing, view);
+            rlist = $A($R(tmp.start, tmp.end)).diff(b.getAllRows());
+
+            if (!rlist.size()) {
+                this.isbusy = false;
+                return;
+            }
+
+            /* Add rows to the loading list for the view. */
+            rlist = rlist.diff(lrows).numericSort();
+            llist.set(r_id, rlist);
+            b.setMetaData({ llist: llist }, true);
+
+            params.update({ slice: rlist.first() + ':' + rlist.last() });
+        }
+
+        if (opts.callback) {
+            tmp = b.getMetaData('callback') || $H();
+            tmp.set(r_id, opts.callback);
+            b.setMetaData({ callback: tmp }, true);
+        }
+
+        if (!opts.background) {
+            this.active_req = r_id;
+            this._handleWait();
+        }
+
+        this._ajaxRequest(params, { noslice: true, view: view });
+
+        this.isbusy = false;
+    },
+
+    // rownum = (integer) Row number
+    // nearing = (string) 'bottom', 'top', null
+    // view = (string) ID of view.
+    _getSliceBounds: function(rownum, nearing, view)
+    {
+        var b_size = this.bufferSize(),
+            ob = {}, trows;
+
+        switch (nearing) {
+        case 'bottom':
+            ob.start = rownum + this.getPageSize();
+            ob.end = ob.start + b_size;
+            break;
+
+        case 'top':
+            ob.start = Math.max(rownum - b_size, 1);
+            ob.end = rownum;
+            break;
+
+        default:
+            ob.start = rownum - this._lookbehind();
+
+            /* Adjust slice if it runs past edge of available rows. In this
+             * case, fetching a tiny buffer isn't as useful as switching
+             * the unused buffer space to the other endpoint. Always allow
+             * searching past the value of total_rows, since the size of the
+             * dataset may have increased. */
+            trows = this.getMetaData('total_rows', view);
+            if (trows) {
+                ob.end = ob.start + b_size;
+
+                if (ob.end > trows) {
+                    ob.start -= ob.end - trows;
+                }
+
+                if (ob.start < 1) {
+                    ob.end += 1 - ob.start;
+                    ob.start = 1;
+                }
+            } else {
+                ob.start = Math.max(ob.start, 1);
+                ob.end = ob.start + b_size;
+            }
+            break;
+        }
+
+        return ob;
+    },
+
+    _lookbehind: function()
+    {
+        return parseInt((this.opts.lookbehind * 0.01) * this.bufferSize(), 10);
+    },
+
+    // args = (object) The list of parameters.
+    // opts = (object) [noslice, view]
+    // Returns a Hash object
+    addRequestParams: function(args, opts)
+    {
+        opts = opts || {};
+        var cid = this.getMetaData('cacheid', opts.view),
+            cached, params, rowlist;
+
+        params = this.opts.onAjaxRequest
+            ? this.opts.onAjaxRequest(opts.view || this.view)
+            : $H();
+
+        params.update({ view: opts.view || this.view });
+
+        if (cid) {
+            params.update({ cacheid: cid });
+        }
+
+        if (!opts.noslice) {
+            rowlist = this._getSliceBounds(this.currentOffset(), null, opts.view);
+            params.update({ slice: rowlist.start + ':' + rowlist.end });
+        }
+
+        if (this.opts.onCachedList) {
+            cached = this.opts.onCachedList(opts.view || this.view);
+        } else {
+            cached = this._getBuffer(opts.view).getAllUIDs();
+            cached = cached.size()
+                ? cached.toJSON()
+                : '';
+        }
+
+        if (cached.length) {
+            params.update({ cache: cached });
+        }
+
+        return params.merge(args);
+    },
+
+    // params - (object) A list of parameters to send to server
+    // opts - (object) Args to pass to addRequestParams().
+    _ajaxRequest: function(params, other)
+    {
+        new Ajax.Request(this.opts.ajax_url, Object.extend(this.opts.ajax_opts || {}, {
+            evalJS: false,
+            evalJSON: true,
+            onComplete: this.ajax_response,
+            onFailure: this.opts.onAjaxFailure || Prototype.emptyFunction,
+            parameters: this.addRequestParams(params, other)
+        }));
+    },
+
+    _ajaxRequestComplete: function(r)
+    {
+        if (r.responseJSON) {
+            this.parseJSONResponse(r.responseJSON);
+        }
+    },
+
+    // r - (object) responseJSON returned from the server.
+    parseJSONResponse: function(r)
+    {
+        if (!r.ViewPort) {
+            return;
+        }
+
+        r = r.ViewPort;
+
+        if (r.rangelist) {
+            this.select(this.createSelection('uid', r.rangelist, r.view));
+            this.opts.container.fire('ViewPort:endFetch', r.view);
+        }
+
+        if (!Object.isUndefined(r.updatecacheid)) {
+            this._getBuffer(r.view).setMetaData({ cacheid: r.updatecacheid }, true);
+        } else if (!Object.isUndefined(r.cacheid)) {
+            this._ajaxResponse(r);
+        }
+    },
+
+    // r = (Object) viewport response object
+    _ajaxResponse: function(r)
+    {
+        if (this.isbusy) {
+            this._ajaxResponse.bind(this, r).defer();
+            return;
+        }
+
+        this.isbusy = true;
+        this._clearWait();
+
+        var callback, offset, tmp,
+            buffer = this._getBuffer(r.view),
+            llist = buffer.getMetaData('llist') || $H(),
+            updated = [];
+
+        buffer.update(Object.isArray(r.data) ? {} : r.data, Object.isArray(r.rowlist) ? {} : r.rowlist, r.metadata || {}, { reset: r.reset, resetmd: r.resetmd, update: r.update });
+
+        if (r.reset) {
+            this.select(new ViewPort_Selection());
+        } else if (r.update && r.disappear && r.disappear.size()) {
+            this.deselect(this.createSelection('uid', r.disappear, r.view));
+            buffer.removeData(r.disappear);
+        }
+
+        llist.unset(r.requestid);
+
+        tmp = {
+            cacheid: r.cacheid,
+            llist: llist,
+            total_rows: r.totalrows
+        };
+        if (r.label) {
+            tmp.label = r.label;
+        }
+        buffer.setMetaData(tmp, true);
+
+        this.opts.container.fire('ViewPort:cacheUpdate', r.view);
+
+        if (r.requestid &&
+            r.requestid == this.active_req) {
+            this.active_req = null;
+            callback = buffer.getMetaData('callback');
+            offset = buffer.getMetaData('req_offset');
+
+            if (callback && callback.get(r.requestid)) {
+                callback.get(r.requestid)(r);
+                callback.unset(r.requestid);
+            }
+
+            buffer.setMetaData({ callback: undefined, req_offset: undefined }, true);
+
+            this.opts.container.fire('ViewPort:endFetch', r.view);
+        }
+
+        if (this.view == r.view) {
+            if (r.update) {
+                updated = this.createSelection('uid', Object.keys(r.data)).get('domid');
+            }
+            this._updateContent(Object.isUndefined(r.rownum) ? (Object.isUndefined(offset) ? this.currentOffset() : offset) : Number(r.rownum) - 1, { updated: updated });
+        } else if (r.rownum) {
+            // We loaded in the background. If rownumber information was
+            // provided, we need to save this or else we will position the
+            // viewport incorrectly.
+            buffer.setMetaData({ offset: Number(r.rownum) - 1 }, true);
+        }
+
+        this.isbusy = false;
+    },
+
+    // offset = (integer) TODO
+    // opts = (object) TODO [background, updated, view]
+    _updateContent: function(offset, opts)
+    {
+        opts = opts || {};
+
+        if (!this._getBuffer(opts.view).sliceLoaded(offset)) {
+            opts.offset = offset;
+            this._fetchBuffer(opts);
+            return false;
+        }
+
+        var added = {},
+            c = this.opts.content,
+            page_size = this.getPageSize(),
+            tmp = [],
+            updated = opts.updated || [],
+            vr = this.visibleRows(),
+            fdiv, rows;
+
+        this.scroller.setSize(page_size, this.getMetaData('total_rows'));
+        this.scrollTo(offset + 1, { noupdate: true, top: true });
+
+        offset = this.currentOffset();
+        if (this.opts.onContentOffset) {
+            offset = this.opts.onContentOffset(offset);
+        }
+
+        rows = this.createSelection('rownum', $A($R(offset + 1, offset + page_size)));
+
+        if (rows.size()) {
+            fdiv = document.createDocumentFragment().appendChild(new Element('DIV'));
+
+            rows.get('dataob').each(function(r) {
+                var elt;
+                if (!updated.include(r.VP_domid) &&
+                    (elt = $(r.VP_domid))) {
+                    tmp.push(elt);
+                } else {
+                    fdiv.insert({ top: this.prepareRow(r) });
+                    added[r.VP_domid] = 1;
+                    tmp.push(fdiv.down());
+                }
+            }, this);
+
+            vr.pluck('id').diff(rows.get('domid')).each($).compact().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
+
+            c.childElements().invoke('remove');
+
+            tmp.each(function(r) {
+                c.insert(r);
+                if (added[r.identify()]) {
+                    this.opts.container.fire('ViewPort:add', r);
+                }
+            }, this);
+        } else {
+            vr.each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear'));
+            vr.invoke('remove');
+            c.update(this.empty_msg);
+        }
+
+        this.scroller.updateDisplay();
+        this.opts.container.fire('ViewPort:contentComplete');
+
+        return true;
+    },
+
+    prepareRow: function(row)
+    {
+        var r = Object.clone(row);
+
+        r.VP_bg = this.getSelected().contains('uid', r.VP_id)
+            ? [ 'vpRowSelected' ]
+            : [];
+
+        return this.opts.onContent(r, this.pane_mode);
+    },
+
+    updateRow: function(row)
+    {
+        var d = $(row.VP_domid);
+        if (d) {
+            this.opts.container.fire('ViewPort:clear', d);
+            d.replace(this.prepareRow(row));
+            this.opts.container.fire('ViewPort:add', $(row.VP_domid));
+        }
+    },
+
+    _handleWait: function(call)
+    {
+        this._clearWait();
+
+        // Server did not respond in defined amount of time.  Alert the
+        // callback function and set the next timeout.
+        if (call) {
+            this.opts.container.fire('ViewPort:wait', this.view);
+        }
+
+        // Call wait handler every x seconds
+        if (this.opts.viewport_wait) {
+            this.waitHandler = this._handleWait.bind(this, true).delay(this.opts.viewport_wait);
+        }
+    },
+
+    _clearWait: function()
+    {
+        if (this.waitHandler) {
+            clearTimeout(this.waitHandler);
+            this.waitHandler = null;
+        }
+    },
+
+    visibleRows: function()
+    {
+        return this.opts.content.select('DIV.vpRow');
+    },
+
+    getMetaData: function(id, view)
+    {
+        return this._getBuffer(view).getMetaData(id);
+    },
+
+    setMetaData: function(vals, view)
+    {
+        this._getBuffer(view).setMetaData(vals, false);
+    },
+
+    _getBuffer: function(view, create)
+    {
+        view = view || this.view;
+
+        return (!create && this.views[view])
+            ? this.views[view]
+            : new ViewPort_Buffer(this, view);
+    },
+
+    currentOffset: function()
+    {
+        return this.scroller.currentOffset();
+    },
+
+    // return: (object) The current viewable range of the viewport.
+    //         first: Top-most row offset
+    //         last: Bottom-most row offset
+    currentViewableRange: function()
+    {
+        var offset = this.currentOffset();
+        return {
+            first: offset + 1,
+            last: Math.min(offset + this.getPageSize(), this.getMetaData('total_rows'))
+        };
+    },
+
+    _getLineHeight: function()
+    {
+        var mode = this.pane_mode || 'horiz';
+
+        if (!this.split_pane[mode].lh) {
+            // To avoid hardcoding the line height, create a temporary row to
+            // figure out what the CSS says.
+            var d = new Element('DIV', { className: this.opts.list_class }).insert(this.prepareRow({ VP_domid: null }, mode)).hide();
+            $(document.body).insert(d);
+            this.split_pane[mode].lh = d.getHeight();
+            d.remove();
+        }
+
+        return this.split_pane[mode].lh;
+    },
+
+    // (type) = (string) [null (DEFAULT), 'current', 'default', 'max']
+    // return: (integer) Number of rows in current view.
+    getPageSize: function(type)
+    {
+        switch (type) {
+        case 'current':
+            return Math.min(this.page_size, this.getMetaData('total_rows'));
+
+        case 'default':
+            return (this.pane_mode == 'vert')
+                ? this.getPageSize('max')
+                : Math.max(parseInt(this.getPageSize('max') * 0.45, 10), 5);
+
+        case 'max':
+        case 'splitmax':
+            return parseInt(this._getMaxHeight() / this._getLineHeight()) - (type == 'max' ? 0 : 1);
+
+        default:
+            return this.page_size;
+        }
+    },
+
+    _getMaxHeight: function()
+    {
+        return document.viewport.getHeight() - this.opts.content.viewportOffset()[1];
+    },
+
+    bufferSize: function()
+    {
+        // Buffer size must be at least the maximum page size.
+        return Math.round(Math.max(this.getPageSize('max') + 1, this.opts.buffer_pages * this.getPageSize()));
+    },
+
+    limitTolerance: function()
+    {
+        return Math.round(this.bufferSize() * (this.opts.limit_factor / 100));
+    },
+
+    // mode = (string) Either 'horiz', 'vert', or empty.
+    showSplitPane: function(mode)
+    {
+        this.pane_mode = mode;
+        this.onResize(true);
+    },
+
+    _initSplitBar: function()
+    {
+        var sp = this.split_pane;
+
+        if (sp.currbar) {
+            sp.currbar.hide();
+        }
+
+        sp.curr = this.pane_mode;
+
+        if (sp[this.pane_mode].bar) {
+            sp.currbar = sp[this.pane_mode].bar.show();
+            return;
+        }
+
+        sp.currbar = sp[this.pane_mode].bar = new Element('DIV', { className: this.opts.split_bar_class[this.pane_mode] });
+
+        if (!this.opts.pane_data.descendantOf(this.opts.container)) {
+            this.opts.container.insert(this.opts.pane_data.remove());
+        }
+
+        this.opts.pane_data.insert({ before: sp.currbar });
+
+        switch (this.pane_mode) {
+        case 'horiz':
+            new Drag(sp.currbar.setStyle({ clear: 'left' }), {
+                constraint: 'vertical',
+                ghosting: true,
+                nodrop: true,
+                snap: function(x, y, elt) {
+                    var sp = this.split_pane,
+                        l = parseInt((y - sp.pos) / sp.lh);
+                    if (l < 1) {
+                        l = 1;
+                    } else if (l > sp.max) {
+                        l = sp.max;
+                    }
+                    sp.lines = l;
+                    return [ x, sp.pos + (l * sp.lh) ];
+                }.bind(this)
+            });
+            break;
+
+        case 'vert':
+            new Drag(sp.currbar.setStyle({ float: 'left' }), {
+                constraint: 'horizontal',
+                ghosting: true,
+                nodrop: true,
+                snapToParent: true
+            });
+            break;
+        }
+
+        if (!sp.init) {
+            document.observe('DragDrop2:end', this._onDragEnd.bindAsEventListener(this));
+            document.observe('DragDrop2:start', this._onDragStart.bindAsEventListener(this));
+            document.observe('dblclick', this._onDragDblClick.bindAsEventListener(this));
+            sp.init = true;
+        }
+    },
+
+    _onDragStart: function(e)
+    {
+        var sp = this.split_pane;
+
+        if (e.element() != sp.currbar) {
+            return;
+        }
+
+        if (this.pane_mode == 'horiz') {
+            // Cache these values since we will be using them multiple
+            // times in snap().
+            sp.lh = this._getLineHeight();
+            sp.lines = this.page_size;
+            sp.max = this.getPageSize('splitmax');
+            sp.orig = this.page_size;
+            sp.pos = this.opts.content.positionedOffset()[1];
+        }
+
+        this.opts.container.fire('ViewPort:splitBarStart', this.pane_mode);
+    },
+
+    _onDragEnd: function(e)
+    {
+        var change, drag,
+            sp = this.split_pane;
+
+        if (e.element() != sp.currbar) {
+            return;
+        }
+
+        switch (this.pane_mode) {
+        case 'horiz':
+            this.onResize(true, sp.lines);
+            change = (sp.orig != sp.lines);
+            break;
+
+        case 'vert':
+            drag = DragDrop.Drags.getDrag(e.element());
+            sp.vert.width = drag.lastCoord[0];
+            this.opts.content.setStyle({ width: sp.vert.width + 'px' });
+            change = drag.wasDragged;
+            break;
+        }
+
+        if (change) {
+            this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode);
+        }
+        this.opts.container.fire('ViewPort:splitBarEnd', this.pane_mode);
+    },
+
+    _onDragDblClick: function(e)
+    {
+        if (e.element() != this.split_pane.currbar) {
+            return;
+        }
+
+        var change, old_size = this.page_size;
+
+        switch (this.pane_mode) {
+        case 'horiz':
+            this.onResize(true, this.getPageSize('default'));
+            change = (old_size != this.page_size);
+            break;
+
+        case 'vert':
+            this.opts.content.setStyle({ width: parseInt(this.opts.container.clientWidth * 0.45, 10) + 'px' });
+            change = true;
+        }
+
+        if (change) {
+            this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode);
+        }
+    },
+
+    getAllRows: function(view)
+    {
+        var buffer = this._getBuffer(view);
+        return buffer
+            ? buffer.getAllRows()
+            : [];
+    },
+
+    createSelection: function(format, data, view)
+    {
+        var buffer = this._getBuffer(view);
+        return buffer
+            ? new ViewPort_Selection(buffer, format, data)
+            : new ViewPort_Selection(this._getBuffer(this.view));
+    },
+
+    getSelection: function(view)
+    {
+        var buffer = this._getBuffer(view);
+        return this.createSelection('uid', buffer ? buffer.getSelected().get('uid') : [], view);
+    },
+
+    // vs = (Viewport_Selection | array) A Viewport_Selection object -or- if
+    //       opts.range is set, an array of row numbers.
+    // opts = (object) TODO [add, range, search]
+    select: function(vs, opts)
+    {
+        opts = opts || {};
+
+        var b = this._getBuffer(),
+            sel, slice;
+
+        if (opts.search) {
+            return this._fetchBuffer({
+                callback: function(r) {
+                    if (r.rownum) {
+                        this.select(this.createSelection('rownum', [ r.rownum ]), { add: opts.add, range: opts.range });
+                    }
+                }.bind(this),
+                search: opts.search
+            });
+        }
+
+        if (opts.range) {
+            slice = this.createSelection('rownum', vs);
+            if (vs.size() != slice.size()) {
+                this.opts.container.fire('ViewPort:fetch', this.view);
+                return this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() });
+            }
+            vs = slice;
+        }
+
+        if (!opts.add) {
+            sel = this.getSelected();
+            b.deselect(sel, true);
+            sel.get('div').invoke('removeClassName', 'vpRowSelected');
+        }
+        b.select(vs);
+        vs.get('div').invoke('addClassName', 'vpRowSelected');
+        this.opts.container.fire('ViewPort:select', { opts: opts, vs: vs });
+    },
+
+    // vs = (Viewport_Selection) A Viewport_Selection object.
+    // opts = (object) TODO [clearall]
+    deselect: function(vs, opts)
+    {
+        opts = opts || {};
+
+        if (vs.size() &&
+            this._getBuffer().deselect(vs, opts && opts.clearall)) {
+            vs.get('div').invoke('removeClassName', 'vpRowSelected');
+            this.opts.container.fire('ViewPort:deselect', { opts: opts, vs: vs });
+        }
+    },
+
+    getSelected: function()
+    {
+        return Object.clone(this._getBuffer().getSelected());
+    }
+
+}),
+
+/**
+ * ViewPort_Scroller
+ */
+ViewPort_Scroller = Class.create({
+    // Variables initialized to undefined:
+    //   noupdate, scrollDiv, scrollbar, vertscroll, vp
+
+    initialize: function(vp)
+    {
+        this.vp = vp;
+    },
+
+    _createScrollBar: function()
+    {
+        if (this.scrollDiv) {
+            return;
+        }
+
+        var c = this.vp.opts.content;
+
+        // Create the outer div.
+        this.scrollDiv = new Element('DIV', { className: 'vpScroll' }).setStyle({ float: 'left', overflow: 'hidden' }).hide();
+        c.insert({ after: this.scrollDiv });
+
+        this.scrollDiv.observe('Slider2:change', this._onScroll.bind(this));
+        if (this.vp.opts.onSlide) {
+            this.scrollDiv.observe('Slider2:slide', this.vp.opts.onSlide);
+        }
+
+        // Create scrollbar object.
+        this.scrollbar = new Slider2(this.scrollDiv, {
+            buttonclass: { up: 'vpScrollUp', down: 'vpScrollDown' },
+            cursorclass: 'vpScrollCursor',
+            pagesize: this.vp.getPageSize(),
+            totalsize: this.vp.getMetaData('total_rows')
+       });
+
+        // Mouse wheel handler.
+        c.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', function(e) {
+            var move_num = Math.min(this.vp.getPageSize(), 3);
+            this.moveScroll(this.currentOffset() + ((e.wheelDelta >= 0 || e.detail < 0) ? (-1 * move_num) : move_num));
+            /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818
+             * Need to stop or else multiple scroll events may be fired. We
+             * lose the ability to have the mousescroll bubble up, but that is
+             * more desirable than having the wrong scrolling behavior. */
+            if (Prototype.Browser.Gecko && !e.stop) {
+                Event.stop(e);
+            }
+        }.bindAsEventListener(this));
+    },
+
+    setSize: function(viewsize, totalsize)
+    {
+        this._createScrollBar();
+        this.scrollbar.setHandleLength(viewsize, totalsize);
+    },
+
+    updateDisplay: function()
+    {
+        var c = this.vp.opts.content,
+            vs = false;
+
+        if (this.scrollbar.needScroll()) {
+            switch (this.vp.pane_mode) {
+            case 'vert':
+                this.scrollDiv.setStyle({ marginLeft: 0 });
+                if (!this.vertscroll) {
+                    c.setStyle({ width: (c.clientWidth - this.scrollDiv.getWidth()) + 'px' });
+                }
+                vs = true;
+                break;
+
+            case 'horiz':
+            default:
+                this.scrollDiv.setStyle({ marginLeft: '-' + this.scrollDiv.getWidth() + 'px' });
+                break;
+            }
+
+            this.scrollDiv.setStyle({ height: c.clientHeight + 'px' });
+        } else if ((this.vp.pane_mode == 'vert') && this.vertscroll) {
+            c.setStyle({ width: (c.clientWidth + this.scrollDiv.getWidth()) + 'px' });
+        }
+
+        this.vertscroll = vs;
+        this.scrollbar.updateHandleLength();
+    },
+
+    clear: function()
+    {
+        this.setSize(0, 0);
+        this.scrollbar.updateHandleLength();
+    },
+
+    // offset = (integer) Offset to move the scrollbar to
+    moveScroll: function(offset)
+    {
+        this._createScrollBar();
+        this.scrollbar.setScrollPosition(offset);
+    },
+
+    _onScroll: function()
+    {
+        if (!this.noupdate) {
+            this.vp.requestContentRefresh(this.currentOffset());
+        }
+    },
+
+    currentOffset: function()
+    {
+        return this.scrollbar ? this.scrollbar.getValue() : 0;
+    }
+
+}),
+
+/**
+ * ViewPort_Buffer
+ *
+ * Note: recognize the difference between offset (current location in the
+ * viewport - starts at 0) with start parameters (the row numbers - starts
+ * at 1).
+ */
+ViewPort_Buffer = Class.create({
+
+    initialize: function(vp, view)
+    {
+        this.vp = vp;
+        this.view = view;
+        this.clear();
+    },
+
+    getView: function()
+    {
+        return this.view;
+    },
+
+    // d = (object) Data
+    // l = (object) Rowlist
+    // md = (object) User defined metadata
+    // opts = (object) TODO [reset, resetmd, update]
+    update: function(d, l, md, opts)
+    {
+        d = $H(d);
+        l = $H(l);
+        opts = opts || {};
+
+        if (!opts.reset && this.data.size()) {
+            this.data.update(d);
+        } else {
+            this.data = d;
+        }
+
+        if (opts.update || opts.reset) {
+            this.uidlist = l;
+            this.rowlist = $H();
+        } else {
+            this.uidlist = this.uidlist.size() ? this.uidlist.merge(l) : l;
+        }
+
+        l.each(function(o) {
+            this.rowlist.set(o.value, o.key);
+        }, this);
+
+        if (opts.resetmd) {
+            this.usermdata = $H(md);
+        } else {
+            $H(md).each(function(pair) {
+                if (Object.isString(pair.value) ||
+                    Object.isNumber(pair.value) ||
+                    Object.isArray(pair.value)) {
+                    this.usermdata.set(pair.key, pair.value);
+                } else {
+                    var val = this.usermdata.get(pair.key);
+                    if (val) {
+                        this.usermdata.get(pair.key).update($H(pair.value));
+                    } else {
+                        this.usermdata.set(pair.key, $H(pair.value));
+                    }
+                }
+            }, this);
+        }
+    },
+
+    // offset = (integer) Offset of the beginning of the slice.
+    // rows = (array) Additional rows to include in the search.
+    sliceLoaded: function(offset, rows)
+    {
+        var range, tr = this.getMetaData('total_rows');
+
+        // Undefined here indicates we have never sent a previous buffer
+        // request.
+        if (Object.isUndefined(tr)) {
+            return false;
+        }
+
+        range = $A($R(offset + 1, Math.min(offset + this.vp.getPageSize() - 1, tr)));
+
+        return rows
+            ? (range.diff(this.rowlist.keys().concat(rows)).size() == 0)
+            : !this._rangeCheck(range);
+    },
+
+    isNearingLimit: function(offset)
+    {
+        if (this.uidlist.size() != this.getMetaData('total_rows')) {
+            if (offset != 0 &&
+                this._rangeCheck($A($R(Math.max(offset + 1 - this.vp.limitTolerance(), 1), offset)))) {
+                return 'top';
+            } else if (this._rangeCheck($A($R(offset + 1, Math.min(offset + this.vp.limitTolerance() + this.vp.getPageSize() - 1, this.getMetaData('total_rows')))).reverse())) {
+                // Search for missing rows in reverse order since in normal
+                // usage (sequential scrolling through the row list) rows are
+                // more likely to be missing at furthest from the current
+                // view.
+                return 'bottom';
+            }
+        }
+    },
+
+    _rangeCheck: function(range)
+    {
+        return !range.all(this.rowlist.get.bind(this.rowlist));
+    },
+
+    getData: function(uids)
+    {
+        return uids.collect(function(u) {
+            var e = this.data.get(u);
+            if (!Object.isUndefined(e)) {
+                // We can directly write the rownum to the original object
+                // since we will always rewrite when creating rows.
+                e.VP_domid = 'VProw' + this.view + '_' + u;
+                e.VP_rownum = this.uidlist.get(u);
+                e.VP_id = u;
+                return e;
+            }
+        }, this).compact();
+    },
+
+    getAllUIDs: function()
+    {
+        return this.uidlist.keys();
+    },
+
+    getAllRows: function()
+    {
+        return this.rowlist.keys();
+    },
+
+    rowsToUIDs: function(rows)
+    {
+        return rows.collect(this.rowlist.get.bind(this.rowlist)).compact();
+    },
+
+    // vs = (Viewport_Selection) TODO
+    select: function(vs)
+    {
+        this.selected.add('uid', vs.get('uid'));
+    },
+
+    // vs = (Viewport_Selection) TODO
+    // clearall = (boolean) Clear all entries?
+    deselect: function(vs, clearall)
+    {
+        var size = this.selected.size();
+
+        if (clearall) {
+            this.selected.clear();
+        } else {
+            this.selected.remove('uid', vs.get('uid'));
+        }
+        return size != this.selected.size();
+    },
+
+    getSelected: function()
+    {
+        return this.selected;
+    },
+
+    // rownums = (array) Array of row numbers to remove.
+    remove: function(rownums)
+    {
+        var minrow = rownums.min(),
+            rowsize = this.rowlist.size(),
+            rowsubtract = 0,
+            newsize = rowsize - rownums.size();
+
+        return this.rowlist.keys().each(function(n) {
+            if (n >= minrow) {
+                var id = this.rowlist.get(n), r;
+                if (rownums.include(n)) {
+                    this.data.unset(id);
+                    this.uidlist.unset(id);
+                    rowsubtract++;
+                } else if (rowsubtract) {
+                    r = n - rowsubtract;
+                    this.rowlist.set(r, id);
+                    this.uidlist.set(id, r);
+                }
+                if (n > newsize) {
+                    this.rowlist.unset(n);
+                }
+            }
+        }, this);
+    },
+
+    removeData: function(uids)
+    {
+        uids.each(function(u) {
+            this.data.unset(u);
+            this.uidlist.unset(u);
+        }, this);
+    },
+
+    resetRowlist: function()
+    {
+        this.rowlist = $H();
+    },
+
+    clear: function()
+    {
+        this.data = $H();
+        this.mdata = $H({ total_rows: 0 });
+        this.rowlist = $H();
+        this.selected = new ViewPort_Selection(this);
+        this.uidlist = $H();
+        this.usermdata = $H();
+    },
+
+    getMetaData: function(id)
+    {
+        var data = this.mdata.get(id);
+
+        return Object.isUndefined(data)
+            ? this.usermdata.get(id)
+            : data;
+    },
+
+    setMetaData: function(vals, priv)
+    {
+        if (priv) {
+            this.mdata.update(vals);
+        } else {
+            this.usermdata.update(vals);
+        }
+    }
+
+}),
+
+/**
+ * ViewPort_Selection
+ */
+ViewPort_Selection = Class.create({
+
+    // Define property to aid in object detection
+    viewport_selection: true,
+
+    // Formats:
+    //     'dataob' = Data objects
+    //     'div' = DOM DIVs
+    //     'domid' = DOM IDs
+    //     'rownum' = Row numbers
+    //     'uid' = Unique IDs
+    initialize: function(buffer, format, data)
+    {
+        this.buffer = buffer;
+        this.clear();
+        if (!Object.isUndefined(format)) {
+            this.add(format, data);
+        }
+    },
+
+    add: function(format, d)
+    {
+        var c = this._convert(format, d);
+        this.data = this.data.size() ? this.data.concat(c).uniq() : c;
+    },
+
+    remove: function(format, d)
+    {
+        this.data = this.data.diff(this._convert(format, d));
+    },
+
+    _convert: function(format, d)
+    {
+        d = Object.isArray(d) ? d : [ d ];
+
+        // Data is stored internally as UIDs.
+        switch (format) {
+        case 'dataob':
+            return d.pluck('VP_id');
+
+        case 'div':
+            // ID here is the DOM ID of the element object.
+            d = d.pluck('id');
+            // Fall-through
+
+        case 'domid':
+            return d.invoke('substring', 6 + this.buffer.getView().length);
+
+        case 'rownum':
+            return this.buffer.rowsToUIDs(d);
+
+        case 'uid':
+            return d;
+        }
+    },
+
+    clear: function()
+    {
+        this.data = [];
+    },
+
+    get: function(format)
+    {
+        format = Object.isUndefined(format) ? 'uid' : format;
+        if (format == 'uid') {
+            return this.data;
+        }
+        var d = this.buffer.getData(this.data);
+
+        switch (format) {
+        case 'dataob':
+            return d;
+
+        case 'div':
+            return d.pluck('VP_domid').collect(function(e) { return $(e); }).compact();
+
+        case 'domid':
+            return d.pluck('VP_domid');
+
+        case 'rownum':
+            return d.pluck('VP_rownum');
+        }
+    },
+
+    contains: function(format, d)
+    {
+        return this.data.include(this._convert(format, d).first());
+    },
+
+    // params = (Object) Key is search key, value is object -> key of object
+    // must be the following:
+    //   equal - Matches any value contained in the query array.
+    //   include - Matches if this value is contained within the array.
+    //   notequal - Matches any value not contained in the query array.
+    //   notinclude - Matches if this value is not contained within the array.
+    //   regex - Matches the RegExp contained in the query.
+    search: function(params)
+    {
+        return new ViewPort_Selection(this.buffer, 'uid', this.get('dataob').findAll(function(i) {
+            // i = data object
+            return $H(params).all(function(k) {
+                // k.key = search key; k.value = search criteria
+                return $H(k.value).all(function(s) {
+                    // s.key = search type; s.value = search query
+                    switch (s.key) {
+                    case 'equal':
+                    case 'notequal':
+                        var r = i[k.key] && s.value.include(i[k.key]);
+                        return (s.key == 'equal') ? r : !r;
+
+                    case 'include':
+                    case 'notinclude':
+                        var r = i[k.key] && Object.isArray(i[k.key]) && i[k.key].include(s.value);
+                        return (s.key == 'include') ? r : !r;
+
+                    case 'regex':
+                        return i[k.key].match(s.value);
+                    }
+                });
+            });
+        }).pluck('VP_id'));
+    },
+
+    size: function()
+    {
+        return this.data.size();
+    },
+
+    set: function(vals)
+    {
+        this.get('dataob').each(function(d) {
+            $H(vals).each(function(v) {
+                d[v.key] = v.value;
+            });
+        });
+    },
+
+    getBuffer: function()
+    {
+        return this.buffer;
+    }
+
+});
+
+/** Utility Functions **/
+Object.extend(Array.prototype, {
+    // Need our own diff() function because prototypejs's without() function
+    // does not handle array input.
+    diff: function(values)
+    {
+        return this.select(function(value) {
+            return !values.include(value);
+        });
+    },
+    numericSort: function()
+    {
+        return this.collect(Number).sort(function(a, b) {
+            return (a > b) ? 1 : ((a < b) ? -1 : 0);
+        });
+    }
+});
index c30d094..9fcb256 100644 (file)
@@ -466,8 +466,8 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
      * <pre>
      * 'checkcache' - (integer) If 1, only send data if cache has been
      *                invalidated.
-     * 'rangeslice' - (string) Range slice. See js/ViewPort.js.
-     * 'requestid' - (string) Request ID. See js/ViewPort.js.
+     * 'rangeslice' - (string) Range slice. See js/viewport.js.
+     * 'requestid' - (string) Request ID. See js/viewport.js.
      * 'sortby' - (integer) The Horde_Imap_Client sort constant.
      * 'sortdir' - (integer) 0 for ascending, 1 for descending.
      * 'view' - (string) The current full mailbox name.
@@ -1695,7 +1695,7 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
     }
 
     /**
-     * Generates the delete data needed for DimpBase.js.
+     * Generates the delete data needed for dimpbase.js.
      *
      * See the list of variables needed for _viewPortData().
      *
index f68b6ff..8210e58 100644 (file)
@@ -58,8 +58,8 @@ class IMP_Dimp
         $core_scripts = array(
             array('effects.js', 'horde'),
             array('horde.js', 'horde'),
-            array('DimpCore.js', 'imp'),
-            array('Growler.js', 'horde')
+            array('dimpcore.js', 'imp'),
+            array('growler.js', 'horde')
         );
         foreach (array_merge($core_scripts, $scripts) as $val) {
             call_user_func_array(array('Horde', 'addScriptFile'), $val);
index c03e68b..3a5c9a8 100644 (file)
@@ -56,8 +56,8 @@ if (isset($show_msg_result['error'])) {
 }
 
 $scripts = array(
-    array('ContextSensitive.js', 'horde'),
-    array('TextareaResize.js', 'horde'),
+    array('contextsensitive.js', 'horde'),
+    array('textarearesize.js', 'horde'),
     array('fullmessage-dimp.js', 'imp'),
     array('imp.js', 'imp'),
     array('md5.js', 'horde')
index aa04165..0107773 100644 (file)
@@ -96,7 +96,7 @@ $template->set('notify', Horde::endBuffer());
 $title = _("Feeds");
 Horde::addScriptFile('prototype.js', 'horde', true);
 Horde::addScriptFile('tables.js', 'horde', true);
-Horde::addScriptFile('QuickFinder.js', 'horde', true);
+Horde::addScriptFile('quickfinder.js', 'horde', true);
 require JONAH_TEMPLATES . '/common-header.inc';
 echo $template->fetch(JONAH_TEMPLATES . '/channels/index.html');
 require $registry->get('templates', 'horde') . '/common-footer.inc';
index e2c0e5b..fadd0ab 100644 (file)
@@ -69,8 +69,8 @@ class Kronolith
         Horde::addScriptFile('sound.js', 'horde');
         Horde::addScriptFile('horde.js', 'horde');
         Horde::addScriptFile('dragdrop2.js', 'horde');
-        Horde::addScriptFile('Growler.js', 'horde');
-        Horde::addScriptFile('dhtmlHistory.js', 'horde');
+        Horde::addScriptFile('growler.js', 'horde');
+        Horde::addScriptFile('dhtmlhistory.js', 'horde');
         Horde::addScriptFile('redbox.js', 'horde');
         Horde::addScriptFile('tooltips.js', 'horde');
         Horde::addScriptFile('colorpicker.js', 'horde');
index 80bc462..0762f78 100644 (file)
@@ -7,7 +7,7 @@ function toggleTags(domid)
 }
 </script>
 <?php
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 Horde::addScriptFile('redbox.js', 'horde');
 Horde::addScriptFile('calendar-panel.js', 'kronolith');
 
index 814636d..b315aa6 100644 (file)
@@ -57,7 +57,7 @@ case 'search_memos':
 Horde::addScriptFile('tooltips.js', 'horde', true);
 Horde::addScriptFile('tables.js', 'horde', true);
 Horde::addScriptFile('prototype.js', 'horde', true);
-Horde::addScriptFile('QuickFinder.js', 'horde', true);
+Horde::addScriptFile('quickfinder.js', 'horde', true);
 require MNEMO_TEMPLATES . '/common-header.inc';
 require MNEMO_TEMPLATES . '/menu.inc';
 $notification->notify();
index 7d01021..228c838 100644 (file)
@@ -40,7 +40,7 @@ $memos = $search_results;
 Horde::addScriptFile('tooltips.js', 'horde', true);
 Horde::addScriptFile('tables.js', 'horde', true);
 Horde::addScriptFile('prototype.js', 'horde', true);
-Horde::addScriptFile('QuickFinder.js', 'horde', true);
+Horde::addScriptFile('quickfinder.js', 'horde', true);
 
 require MNEMO_TEMPLATES . '/common-header.inc';
 require MNEMO_TEMPLATES . '/menu.inc';
index df3d74a..d0df3a8 100644 (file)
@@ -87,7 +87,7 @@ default:
 
 Horde::addScriptFile('tooltips.js', 'horde');
 Horde::addScriptFile('effects.js', 'horde');
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 
 require NAG_TEMPLATES . '/common-header.inc';
 require NAG_TEMPLATES . '/menu.inc';
index b8a0de5..16dfc98 100644 (file)
@@ -47,7 +47,7 @@ $actionID = null;
 
 Horde::addScriptFile('tooltips.js', 'horde');
 Horde::addScriptFile('effects.js', 'horde');
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 
 require NAG_TEMPLATES . '/common-header.inc';
 require NAG_TEMPLATES . '/menu.inc';
index 9072af1..4dddfae 100644 (file)
@@ -100,7 +100,7 @@ default:
 
 Horde::addScriptFile('tooltips.js', 'horde');
 Horde::addScriptFile('effects.js', 'horde');
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 
 require SKOLI_TEMPLATES . '/common-header.inc';
 require SKOLI_TEMPLATES . '/menu.inc';
index 551a5b8..e2a5e85 100644 (file)
@@ -120,7 +120,7 @@ if ($conf['objects']['allow_absences']) {
 $title = _("Search");
 $notification->push('document.skoli_searchform.stext.focus();', 'javascript');
 
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 Horde::addScriptFile('effects.js', 'horde');
 Horde::addScriptFile('redbox.js', 'horde');
 require SKOLI_TEMPLATES . '/common-header.inc';
index e67660e..ead9ed5 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 
 $current_user = Horde_Auth::getAuth();
 $my_classes = array();
index bf8e983..e77548e 100644 (file)
@@ -400,7 +400,7 @@ class Turba_View_Browse {
             $templates[] = '/browse/header.inc';
         }
 
-        Horde::addScriptFile('QuickFinder.js', 'horde');
+        Horde::addScriptFile('quickfinder.js', 'horde');
         Horde::addScriptFile('effects.js', 'horde');
         Horde::addScriptFile('redbox.js', 'horde');
         require TURBA_TEMPLATES . '/common-header.inc';
index 7a9a929..5a71ad7 100644 (file)
@@ -183,7 +183,7 @@ if ($_SESSION['turba']['search_mode'] == 'basic') {
     $notification->push('document.directory_search.name.focus();', 'javascript');
 }
 
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
 Horde::addScriptFile('effects.js', 'horde');
 Horde::addScriptFile('redbox.js', 'horde');
 require TURBA_TEMPLATES . '/common-header.inc';