}
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';
// 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';
$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';
{
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'],
+++ /dev/null
-/**
- * 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');
- }
- }
-
-});
+++ /dev/null
-/**
- * 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("×");
- 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("×");
- 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();
- }
-
- });
-
-})();
+++ /dev/null
-/**
- * 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(' ', ' ');
- }
- 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;
- }
-
-});
+++ /dev/null
-/**
- * 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));
-});
+++ /dev/null
-/**
- * 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;
- }
-
-});
+++ /dev/null
-/**
- * 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');
- }
- }
-
-});
/**
* 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
--- /dev/null
+/**
+ * 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');
+ }
+ }
+
+});
+++ /dev/null
-/**
- * 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();
- }
-
-};
--- /dev/null
+/**
+ * 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();
+ }
+
+};
--- /dev/null
+/**
+ * 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("×");
+ 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("×");
+ 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();
+ }
+
+ });
+
+})();
+++ /dev/null
-/**
- * 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; });
- });
- });
-}
--- /dev/null
+/**
+ * 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; });
+ });
+ });
+}
--- /dev/null
+/**
+ * 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(' ', ' ');
+ }
+ 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;
+ }
+
+});
--- /dev/null
+/**
+ * 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));
+});
--- /dev/null
+/**
+ * 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');
+ }
+ }
+
+});
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')) &&
Horde::logMessage($e, 'ERR');
}
}
- Horde::addScriptFile('ieEscGuard.js', 'horde');
+ Horde::addScriptFile('ieescguard.js', 'horde');
}
}
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'),
+++ /dev/null
-/**
- * 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(' ').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));
+++ /dev/null
-/**
- * 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) {}
- }
-
-};
+++ /dev/null
-/**
- * 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);
- });
- }
-});
--- /dev/null
+/**
+ * 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(' ').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));
--- /dev/null
+/**
+ * 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) {}
+ }
+
+};
--- /dev/null
+/**
+ * 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);
+ });
+ }
+});
* <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.
}
/**
- * Generates the delete data needed for DimpBase.js.
+ * Generates the delete data needed for dimpbase.js.
*
* See the list of variables needed for _viewPortData().
*
$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);
}
$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')
$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';
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');
}
</script>
<?php
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
Horde::addScriptFile('redbox.js', 'horde');
Horde::addScriptFile('calendar-panel.js', 'kronolith');
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();
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';
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';
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';
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';
$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';
<?php
-Horde::addScriptFile('QuickFinder.js', 'horde');
+Horde::addScriptFile('quickfinder.js', 'horde');
$current_user = Horde_Auth::getAuth();
$my_classes = array();
$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';
$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';