From: Jan Schneider Date: Thu, 20 May 2010 22:19:03 +0000 (+0200) Subject: Consistently use lowercase file names. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=76c1c091e8c027ce77dea8d76ceb2fef5d4cecb4;p=horde.git Consistently use lowercase file names. --- diff --git a/ansel/js/embed.js b/ansel/js/embed.js old mode 100755 new mode 100644 diff --git a/ansel/js/lightbox.js b/ansel/js/lightbox.js old mode 100755 new mode 100644 diff --git a/ansel/js/slideshow.js b/ansel/js/slideshow.js old mode 100755 new mode 100644 diff --git a/ansel/js/slugcheck.js b/ansel/js/slugcheck.js old mode 100755 new mode 100644 diff --git a/ansel/js/tagactions.js b/ansel/js/tagactions.js old mode 100755 new mode 100644 diff --git a/ansel/js/togglewidget.js b/ansel/js/togglewidget.js old mode 100755 new mode 100644 diff --git a/chora/browsefile.php b/chora/browsefile.php index 74f324b10..0748e58c2 100644 --- a/chora/browsefile.php +++ b/chora/browsefile.php @@ -48,7 +48,7 @@ if ($VC->hasFeature('branches')) { } Horde::addScriptFile('tables.js', 'horde'); -Horde::addScriptFile('QuickFinder.js', 'horde'); +Horde::addScriptFile('quickfinder.js', 'horde'); Horde::addScriptFile('revlog.js', 'chora'); require CHORA_TEMPLATES . '/common-header.inc'; require CHORA_TEMPLATES . '/menu.inc'; diff --git a/chora/patchsets.php b/chora/patchsets.php index 91b4863af..7bc7196c0 100644 --- a/chora/patchsets.php +++ b/chora/patchsets.php @@ -52,7 +52,7 @@ Horde::addScriptFile('tables.js', 'horde'); // JS search not needed if showing a single patchset if ($ps_id) { - Horde::addScriptFile('QuickFinder.js', 'horde'); + Horde::addScriptFile('quickfinder.js', 'horde'); } require CHORA_TEMPLATES . '/common-header.inc'; diff --git a/framework/Ajax/lib/Horde/Ajax/Imple/AutoCompleter.php b/framework/Ajax/lib/Horde/Ajax/Imple/AutoCompleter.php index 951333862..8d61c11de 100644 --- a/framework/Ajax/lib/Horde/Ajax/Imple/AutoCompleter.php +++ b/framework/Ajax/lib/Horde/Ajax/Imple/AutoCompleter.php @@ -46,7 +46,7 @@ abstract class Horde_Ajax_Imple_AutoCompleter extends Horde_Ajax_Imple_Base $config = $this->_attach(array('tokens' => array(',', ';'))); Horde::addScriptFile('autocomplete.js', 'horde'); - Horde::addScriptFile('KeyNavList.js', 'horde'); + Horde::addScriptFile('keynavlist.js', 'horde'); Horde::addScriptFile('liquidmetal.js', 'horde'); if (isset($config['ajax'])) { $func = 'Ajax.Autocompleter'; diff --git a/framework/Ajax/lib/Horde/Ajax/Imple/SpellChecker.php b/framework/Ajax/lib/Horde/Ajax/Imple/SpellChecker.php index be51f6de0..81e7962a2 100644 --- a/framework/Ajax/lib/Horde/Ajax/Imple/SpellChecker.php +++ b/framework/Ajax/lib/Horde/Ajax/Imple/SpellChecker.php @@ -57,8 +57,8 @@ class Horde_Ajax_Imple_SpellChecker extends Horde_Ajax_Imple_Base { Horde::addScriptFile('prototype.js', 'horde'); Horde::addScriptFile('effects.js', 'horde'); - Horde::addScriptFile('KeyNavList.js', 'horde'); - Horde::addScriptFile('SpellChecker.js', 'horde'); + Horde::addScriptFile('keynavlist.js', 'horde'); + Horde::addScriptFile('spellchecker.js', 'horde'); $opts = array( 'locales' => $this->_params['locales'], diff --git a/horde/js/ContextSensitive.js b/horde/js/ContextSensitive.js deleted file mode 100644 index 8c76ae927..000000000 --- a/horde/js/ContextSensitive.js +++ /dev/null @@ -1,388 +0,0 @@ -/** - * ContextSensitive: a library for generating context-sensitive content on - * HTML elements. It will take over the click/oncontextmenu functions for the - * document, and works only where these are possible to override. It allows - * contextmenus to be created via both a left and right mouse click. - * - * On Opera, the context menu is triggered by a left click + SHIFT + CTRL - * combination. - * - * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only). - * - * - * Usage: - * ------ - * cs = new ContextSensitive(); - * - * Custom Events: - * -------------- - * Custom events are triggered on the base element. The parameters given - * below are available through the 'memo' property of the Event object. - * - * ContextSensitive:click - * Fired when a contextmenu element is clicked on. - * params: (object) elt - (Element) The menu element clicked on. - * trigger - (string) The parent menu. - * - * ContextSensitive:show - * Fired before a contextmenu is displayed. - * params: (string) The DOM ID of the context menu. - * - * - * Original code by Havard Eide (http://eide.org/) released under the MIT - * license. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Chuck Hagenbuch - * @author Michael Slusarz - */ - -var ContextSensitive = Class.create({ - - initialize: function() - { - this.baseelt = null; - this.current = []; - this.elements = $H(); - this.submenus = $H(); - this.triggers = []; - - if (!Prototype.Browser.Opera) { - document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this)); - } - document.observe('click', this._leftClickHandler.bindAsEventListener(this)); - document.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousescroll', this.close.bind(this)); - }, - - /** - * Elements are of type ContextSensitive.Element. - */ - addElement: function(id, target, opts) - { - var left = Boolean(opts.left); - if (id && !this.validElement(id, left)) { - this.elements.set(id + Number(left), new ContextSensitive.Element(id, target, opts)); - } - }, - - /** - * Remove a registered element. - */ - removeElement: function(id) - { - this.elements.unset(id + '0'); - this.elements.unset(id + '1'); - }, - - /** - * Hide the currently displayed element(s). - */ - close: function() - { - this._closeMenu(0, true); - }, - - /** - * Close all menus below a specified level. - */ - _closeMenu: function(idx, immediate) - { - if (this.current.size()) { - this.current.splice(idx, this.current.size() - idx).each(function(s) { - // Fade-out on final display. - if (!immediate && idx == 0) { - s.fade({ duration: 0.15 }); - } else { - $(s).hide(); - } - }); - - this.triggers.splice(idx, this.triggers.size() - idx).each(function(s) { - $(s).removeClassName('contextHover'); - }); - - if (idx == 0) { - this.baseelt = null; - } - } - }, - - /** - * Returns the current displayed menu element ID, if any. If more than one - * submenu is open, returns the last ID opened. - */ - currentmenu: function() - { - return this.current.last(); - }, - - /** - * Get a valid element (the ones that can be right-clicked) based - * on a element ID. - */ - validElement: function(id, left) - { - return this.elements.get(id + Number(Boolean(left))); - }, - - /** - * Set the disabled flag of an event. - */ - disable: function(id, left, disable) - { - var e = this.validElement(id, left); - if (e) { - e.disable = disable; - } - }, - - /** - * Called when a left click event occurs. Will return before the - * element is closed if we click on an element inside of it. - */ - _leftClickHandler: function(e) - { - var base, elt, elt_up, trigger; - - if (this.operaCheck(e)) { - this._rightClickHandler(e, false); - e.stop(); - return; - } - - // Check for a right click. FF on Linux triggers an onclick event even - // w/a right click, so disregard. - if (e.isRightClick()) { - return; - } - - // Check for click in open contextmenu. - if (this.current.size()) { - elt = e.element(); - if (!elt.match('A')) { - elt = elt.up('A'); - if (!elt) { - this._rightClickHandler(e, true); - return; - } - } - elt_up = elt.up('.contextMenu'); - - if (elt_up) { - e.stop(); - - if (elt.hasClassName('contextSubmenu') && - elt_up.identify() != this.currentmenu()) { - this._closeMenu(this.current.indexOf(elt.identify())); - } - - base = this.baseelt; - trigger = this.triggers.last(); - this.close(); - base.fire('ContextSensitive:click', { elt: elt, trigger: trigger }); - return; - } - } - - // Check if the mouseclick is registered to an element now. - this._rightClickHandler(e, true); - }, - - /** - * Checks if the Opera right-click emulation is present. - */ - operaCheck: function(e) - { - return Prototype.Browser.Opera && e.shiftKey && e.ctrlKey; - }, - - /** - * Called when a right click event occurs. - */ - _rightClickHandler: function(e, left) - { - if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) { - e.stop(); - } - }, - - /** - * Display context menu if valid element has been activated. - */ - trigger: function(target, leftclick, x, y) - { - var ctx, el, offset, offsets, voffsets; - - [ target ].concat(target.ancestors()).find(function(n) { - ctx = this.validElement(n.id, leftclick); - return ctx; - }, this); - - // Try to retrieve the context-sensitive element we want to - // display. If we can't find it we just return. - if (!ctx || - ctx.disable || - !(el = $(ctx.ctx)) || - (leftclick && target == this.baseelt) || - this.currentmenu() == ctx.ctx) { - this.close(); - return false; - } - - this.close(); - - // Register the element that was clicked on. - this.baseelt = target; - - offset = ctx.opts.offset; - if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) { - offset = target.identify(); - } - offset = $(offset); - - if (offset) { - offsets = offset.viewportOffset(); - voffsets = document.viewport.getScrollOffsets(); - x = offsets[0] + voffsets.left; - y = offsets[1] + offset.getHeight() + voffsets.top; - } - - this._displayMenu(el, x, y); - this.triggers.push(el.identify()); - - return true; - }, - - /** - * Display the [sub]menu on the screen. - */ - _displayMenu: function(elt, x, y) - { - // Get window/element dimensions - var eltL, h, w, - id = elt.identify(), - v = document.viewport.getDimensions(); - - elt.setStyle({ visibility: 'hidden' }).show(); - eltL = elt.getLayout(), - h = eltL.get('border-box-height'); - w = eltL.get('border-box-width'); - elt.hide().setStyle({ visibility: 'visible' }); - - // Make sure context window is entirely on screen - if ((y + h) > v.height) { - y = v.height - h - 2; - } - - if ((x + w) > v.width) { - x = this.current.size() - ? ($(this.current.last()).viewportOffset()[0] - w) - : (v.width - w - 2); - } - - this.baseelt.fire('ContextSensitive:show', id); - - elt.setStyle({ left: x + 'px', top: y + 'px' }) - - if (this.current.size()) { - elt.show(); - } else { - // Fade-in on initial display. - elt.appear({ duration: 0.15 }); - } - - this.current.push(id); - }, - - /** - * Add a submenu to an existing menu. - */ - addSubMenu: function(id, submenu) - { - if (!this.submenus.get(id)) { - if (!this.submenus.size()) { - document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this)); - } - this.submenus.set(id, submenu); - $(submenu).addClassName('contextMenu'); - $(id).addClassName('contextSubmenu'); - } - }, - - /** - * Mouseover DOM Event handler. - */ - _mouseoverHandler: function(e) - { - if (!this.current.size()) { - return; - } - - var cm = this.currentmenu(), - elt = e.element(), - elt_up = elt.up('.contextMenu'), - id = elt.identify(), - id_div, offsets, sub, voffsets, x, y; - - if (!elt_up) { - return; - } - - id_div = elt_up.identify(); - - if (elt.hasClassName('contextSubmenu')) { - sub = this.submenus.get(id); - if (sub != cm || this.currentmenu() != id) { - if (id_div != cm) { - this._closeMenu(this.current.indexOf(id_div) + 1); - } - - offsets = elt.viewportOffset(); - voffsets = document.viewport.getScrollOffsets(); - x = offsets[0] + voffsets.left + elt.getWidth(); - y = offsets[1] + voffsets.top; - this._displayMenu($(sub), x, y, id); - this.triggers.push(id); - elt.addClassName('contextHover'); - } - } else if ((this.current.size() > 1) && - id_div != cm) { - this._closeMenu(this.current.indexOf(id)); - } - } - -}); - -ContextSensitive.Element = Class.create({ - - // opts: 'left' -> monitor left click; 'offset' -> id of element used to - // determine offset placement - initialize: function(id, target, opts) - { - this.id = id; - this.ctx = target; - this.opts = opts; - this.opts.left = Boolean(opts.left); - this.disable = false; - - target = $(target); - if (target) { - target.addClassName('contextMenu'); - } - } - -}); diff --git a/horde/js/Growler.js b/horde/js/Growler.js deleted file mode 100644 index 02ace41cc..000000000 --- a/horde/js/Growler.js +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Growler.js - Display 'Growl'-like notifications. - * - * Notice Options (passed to 'Growler.growl()'): - * 'className' - (string) An optional additional CSS class to apply to notice. - * 'header' - (string) The title that is displayed for the notice. - * 'life' - (float) The number of seconds in which the notice remains visible. - * 'log' - (boolean) If true, will log the entry. - * 'speedin' - (float) The speed in seconds in which the notice is shown. - * 'speedout' - (float) The speed in seconds in which the notice is hidden. - * 'sticky' - (boolean) Determines if the notice should always remain visible - * until closed by the user. - * - * Growler Options (passed to 'new Growler()'): - * 'location' - (string) The location of the growler notices. This can be: - * tr (top-right) - * br (bottom-right) - * tl (top-left) - * bl (bottom-left) - * tc (top-center) - * bc (bottom-center) - * 'log' - (boolean) Enable logging. - * 'noalerts' - (string) The localized string to display when no log entries - * are present. - * - * Custom Events: - * -------------- - * Custom events are triggered on the notice element. The parameters given - * below are available through the 'memo' property of the Event object. - * - * Growler:created - * Fired on TODO - * params: NONE - * - * Growler:destroyed - * Fired on TODO - * params: NONE - * - * - * Growler has been tested with Safari 3(Mac|Win), Firefox 3(Mac|Win), IE6, - * IE7, and Opera. - * - * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only). - * - * Code adapted from k.Growler v1.0.0 - * http://code.google.com/p/kproto/ - * Written by Kevin Armstrong - * Released under the MIT license - * - * @author Michael Slusarz - */ - -(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(); - } - - }); - -})(); diff --git a/horde/js/KeyNavList.js b/horde/js/KeyNavList.js deleted file mode 100644 index 58c4c276c..000000000 --- a/horde/js/KeyNavList.js +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Reuseable keyboard or mouse driven list component. Based on - * Scriptaculous' AutoCompleter. - * - * Requires: prototype.js (v1.6.1+) - * - * Usage: - * ====== - * var knl = new KeyNavList(base, { - * 'esc' - (boolean) Escape the displayed output? - * 'list' - (array) Array of objects with the following keys: - * 'l' - (label) Display data - * 's' - (selected) True if this entry should be selected - * by default - * 'v' - (value) Value of entry - * 'onChoose' - (function) Called when an entry is selected. Passed the - * entry value. - * 'onHide' - (function) Called when the list is hidden. Passed the - * list container element. - * 'onShow' - (function) Called when the list is shown. Passed the - * list container element. - * 'domParent' - (Element) Specifies the parent element. Defaults to - * document.body - * 'keydownObserver - (Element) The element to register the keydown handler - * on. Defaults to document. - * }); - * - * [base = (Element) The element to use for display positioning purposes] - * - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * See the enclosed file COPYING for license information (GPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. - */ - -var KeyNavList = Class.create({ - - // Vars used: active, div, iefix, ignore, onClickFunc, onKeyDownFunc, - // resizeFunc, selected - - initialize: function(base, opts) - { - this.active = false; - this.base = $(base); - - this.opts = Object.extend({ - onChoose: Prototype.emptyFunction, - onHide: Prototype.emptyFunction, - onShow: Prototype.emptyFunction, - domParent: null, - keydownObserver: document - }, opts || {}); - - this.div = new Element('DIV', { className: 'KeyNavList' }).hide().insert(new Element('UL')); - - if (!this.opts.domParent) { - this.opts.domParent = document.body; - } - this.opts.domParent = $(this.opts.domParent); - - this.opts.domParent.insert(this.div); - - if (this.opts.list) { - this.update(this.opts.list); - delete this.opts.list; - } - - this.onClickFunc = this.onClick.bindAsEventListener(this); - document.observe('click', this.onClickFunc); - - this.onKeyDownFunc = this.onKeyDown.bindAsEventListener(this); - $(this.opts.keydownObserver).observe('keydown', this.onKeyDownFunc); - - this.resizeFunc = this.hide.bind(this); - Event.observe(window, 'resize', this.resizeFunc); - - if (Prototype.Browser.IE && !window.XMLHttpRequest) { - this.iefix = $('knl_iframe_iefix'); - if (!this.iefix) { - this.iefix = new Element('IFRAME', { id: 'knl_iframe_iefix', src: 'javascript:false;', scrolling: 'no', frameborder: 0 }).setStyle({ position: 'absolute', display: 'block', zIndex: 99 }).hide(); - document.body.appendChild(this.iefix); - } - this.div.setStyle({ zIndex: 100 }); - } - }, - - destroy: function() - { - document.stopObserving('click', this.onClickFunc); - document.stopObserving('keydown', this.onKeyDownFunc); - Event.stopObserving(window, 'resize', this.resizeFunc); - this.div.remove(); - if (this.iefix) { - this.iefix.remove(); - } - }, - - update: function(list) - { - var df = document.createDocumentFragment(); - - list.each(function(v) { - var li; - if (this.opts.esc) { - v.l = v.l.escapeHTML().gsub(' ', '  '); - } - li = new Element('LI').insert(v.l).store('v', v.v); - if (v.s) { - this.markSelected(li); - } - df.appendChild(li); - }.bind(this)); - - this.div.down().childElements().invoke('remove'); - this.div.down().appendChild(df); - }, - - updateBase: function(base) - { - this.base = $(base); - }, - - show: function(list) - { - this.active = true; - - if (!Object.isUndefined(list)) { - this.update(list); - } else if (this.div.visible()) { - return; - } - - this.opts.onShow(this.div); - - this.div.setStyle({ height: null, width: null, top: null }).clonePosition(this.base, { - setHeight: false, - setWidth: false, - offsetTop: this.base.getHeight() - }); - - if (this.div.visible()) { - this._sizeDiv(); - } else { - this.div.appear({ - afterFinish: function() { - if (this.selected) { - this.div.scrollTop = this.selected.offsetTop; - } - }.bind(this), - afterSetup: this._sizeDiv.bind(this), - duration: 0.15 - }); - } - }, - - _sizeDiv: function() - { - var divL = this.div.getLayout(), - dl = divL.get('left'), - dt = divL.get('top'), - off = this.opts.domParent.cumulativeOffset(), - v = document.viewport.getDimensions(); - - if ((divL.get('border-box-height') + dt + off.top + 10) > v.height) { - this.div.setStyle({ - height: (v.height - dt - off.top - 10) + 'px', - width: (this.div.scrollWidth + 5) + 'px' - }); - } - - /* Need to do width second - horizontal scrolling might add scroll - * bar. */ - if ((divL.get('border-box-width') + dl + off.left + 5) > v.width) { - dl = (v.width - divL.get('border-box-width') - off.left - 5); - this.div.setStyle({ left: dl + 'px' }); - } - - if (this.iefix) { - this.iefix.clonePosition(this.div); - } - }, - - hide: function() - { - if (this.div.visible()) { - this.active = false; - this.opts.onHide(this.div); - this.div.fade({ duration: 0.15 }); - if (this.iefix) { - this.iefix.hide(); - } - } - }, - - onKeyDown: function(e) - { - if (!this.active) { - return; - } - - switch (e.keyCode) { - case Event.KEY_TAB: - case Event.KEY_RETURN: - this.opts.onChoose(this.getCurrentEntry()); - this.hide(); - e.stop(); - return; - - case Event.KEY_ESC: - this.hide(); - e.stop(); - return; - - case Event.KEY_UP: - this.markPrevious(); - e.stop(); - return; - - case Event.KEY_DOWN: - this.markNext(); - e.stop(); - return; - } - }, - - onClick: function(e) - { - if (this.active && this.ignore != e) { - var elt = e.findElement('LI'); - - if (elt && - (elt == this.div || elt.descendantOf(this.div))) { - this.markSelected(elt); - this.opts.onChoose(this.getCurrentEntry()); - e.stop(); - } - this.hide(); - } - - this.ignore = null; - }, - - ignoreClick: function(e) - { - this.ignore = e; - }, - - setSelected: function(value) - { - this.markSelected(this.div.down().childElements().find(function(e) { - return e.retrieve('v') == value; - })); - }, - - markSelected: function(elt) - { - if (this.selected) { - this.selected.removeClassName('selected'); - } - this.selected = elt - ? elt.addClassName('selected') - : null; - }, - - markPrevious: function() - { - this.markSelected(this.selected ? this.selected.previous() : null); - }, - - markNext: function() - { - var elt = this.selected - ? this.selected.next() - : this.div.down().childElements().first(); - - if (elt) { - this.markSelected(elt); - } - }, - - getCurrentEntry: function() - { - return this.selected - ? this.selected.retrieve('v') - : null; - } - -}); diff --git a/horde/js/QuickFinder.js b/horde/js/QuickFinder.js deleted file mode 100644 index 02332528f..000000000 --- a/horde/js/QuickFinder.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Component for filtering a table or any list of children based on - * the dynamic value of a text input. It requires the prototype.js - * library. - * - * You should define the CSS class .QuickFinderNoMatch to say what - * happens to items that don't match the criteria. A reasonable - * default would be display:none. - * - * This code is heavily inspired by Filterlicious by Gavin - * Kistner. The filterlicious JavaScript file did not have a license; - * however, most of Gavin's code is under the license defined by - * http://phrogz.net/JS/_ReuseLicense.txt, so I'm including that URL - * and Gavin's name as acknowledgements. - * - * @author Chuck Hagenbuch - */ - -var QuickFinder = { - - attachBehavior: function(input) { - var filterTarget = input.readAttribute('for'); - if (!filterTarget) { - return; - } - - if (filterTarget.indexOf(',') != -1) { - input.filterTargets = []; - var targets = filterTarget.split(','); - for (var i = 0; i < targets.length; ++i) { - var t = $(targets[i]); - if (t) { - input.filterTargets.push(t); - } - } - if (!input.filterTargets.size()) { - return; - } - } else { - input.filterTargets = [ $(filterTarget) ]; - if (!input.filterTargets[0]) { - return; - } - } - - var filterEmpty = input.readAttribute('empty'); - if (filterEmpty) { - input.filterEmpty = $(filterEmpty); - } - - input.observe('keyup', this.onKeyUp.bindAsEventListener(this)); - - for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) { - input.filterTargets[i].childElements().each(function(line) { - var filterText = line.filterText || line.readAttribute('filterText'); - if (!filterText) { - line.filterText = line.innerHTML.stripTags(); - } - line.filterText = line.filterText.toLowerCase(); - }); - } - - this.filter(input); - }, - - onKeyUp: function(e) { - var input = e.element(); - if (input.filterTargets) { - this.filter(input); - } - }, - - filter: function(input) { - var matched = 0, - val = input.value.toLowerCase(); - for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) { - input.filterTargets[i].childElements().each(function(line) { - var filterText = line.filterText; - if (filterText.indexOf(val) == -1) { - line.addClassName('QuickFinderNoMatch'); - } else { - ++matched; - line.removeClassName('QuickFinderNoMatch'); - } - }); - } - - try { - if (input.filterEmpty) { - (matched == 0) ? input.filterEmpty.show() : input.filterEmpty.hide(); - } - } catch (e) {} - } - -} - -document.observe('dom:loaded', function() { - $$('input').each(QuickFinder.attachBehavior.bind(QuickFinder)); -}); diff --git a/horde/js/SpellChecker.js b/horde/js/SpellChecker.js deleted file mode 100644 index 2b8a8c67b..000000000 --- a/horde/js/SpellChecker.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * This spell checker was inspired by work done by Garrison Locke, but - * was rewritten almost completely by Chuck Hagenbuch to use - * Prototype/Scriptaculous. - * - * Requires: prototype.js (v1.6.1+), KeyNavList.js - * - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * Custom Events: - * -------------- - * Custom events are triggered on the target element. - * - * 'SpellChecker:after' - * Fired when the spellcheck processing ends. - * - * 'SpellChecker:before' - * Fired before the spellcheck is performed. - * - * 'SpellChecker:noerror' - * Fired when no spellcheck errors are found. - * - * See the enclosed file COPYING for license information (GPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. - */ - -var SpellChecker = Class.create({ - - // Vars used and defaulting to null: - // bad, choices, disabled, htmlAreaParent, lc, locale, reviewDiv, - // statusButton, statusClass, suggestions, target, url - options: {}, - resumeOnDblClick: true, - state: 'CheckSpelling', - - // Options: - // bs = (array) Button states - // locales = (array) List of locales. See KeyNavList for structure. - // sc = (string) Status class - // statusButton = (string/element) DOM ID or element of the status - // button - // target = (string|Element) DOM element containing data - // url = (string) URL of specllchecker handler - initialize: function(opts) - { - var d, lc, tmp, ul; - - this.url = opts.url; - this.target = $(opts.target); - this.statusButton = $(opts.statusButton); - this.buttonStates = opts.bs; - this.statusClass = opts.sc || ''; - this.disabled = false; - - this.options.onComplete = this.onComplete.bind(this); - - document.observe('click', this.onClick.bindAsEventListener(this)); - - if (opts.locales) { - this.lc = new KeyNavList(this.statusButton, { - list: opts.locales, - onChoose: this.setLocale.bindAsEventListener(this) - }); - - this.statusButton.insert({ after: new Element('SPAN', { className: 'spellcheckPopdownImg' }) }); - } - - this.setStatus('CheckSpelling'); - }, - - setLocale: function(locale) - { - this.locale = locale; - }, - - targetValue: function() - { - return Object.isUndefined(this.target.value) - ? this.target.innerHTML - : this.target.value; - }, - - spellCheck: function() - { - this.target.fire('SpellChecker:before'); - - var opts = Object.clone(this.options), - p = $H(), - url = this.url; - - this.setStatus('Checking'); - - p.set(this.target.identify(), this.targetValue()); - opts.parameters = p.toQueryString(); - - if (this.locale) { - url += '/locale=' + this.locale; - } - if (this.htmlAreaParent) { - url += '/html=1'; - } - - new Ajax.Request(url, opts); - }, - - onComplete: function(request) - { - var bad, content, washidden, - i = 0, - result = request.responseJSON; - - if (Object.isUndefined(result)) { - this.setStatus('Error'); - return; - } - - this.suggestions = result.suggestions || []; - - if (!this.suggestions.size()) { - this.setStatus('CheckSpelling'); - this.target.fire('SpellChecker:noerror'); - return; - } - - bad = result.bad || []; - - content = this.targetValue(); - content = this.htmlAreaParent - ? content.replace(/\r?\n/g, '') - : content.replace(/\r?\n/g, '~~~').escapeHTML(); - - $A(bad).each(function(node) { - var re_text = '' + node + ''; - 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, '
'); - } - 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(/
/gi, '~~~').unescapeHTML().replace(/~~~/g, "\n"); - } - this.target.setValue(t); - this.target.enable(); - - if (this.resumeOnDblClick) { - this.reviewDiv.stopObserving('dblclick'); - } - this.reviewDiv.remove(); - this.reviewDiv = null; - - this.setStatus('CheckSpelling'); - - if (!this.htmlAreaParent) { - this.target.show(); - } - - this.target.fire('SpellChecker:after'); - }, - - setStatus: function(state) - { - if (!this.statusButton) { - return; - } - - this.state = state; - switch (this.statusButton.tagName) { - case 'INPUT': - this.statusButton.setValue(this.buttonStates[state]); - break; - - case 'A': - this.statusButton.update(this.buttonStates[state]); - break; - } - this.statusButton.className = this.statusClass + ' spellcheck' + state; - }, - - isActive: function() - { - return this.reviewDiv; - }, - - disable: function(disable) - { - this.disabled = disable; - } - -}); diff --git a/horde/js/TextareaResize.js b/horde/js/TextareaResize.js deleted file mode 100644 index 0946f7e88..000000000 --- a/horde/js/TextareaResize.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * TextareaResize: a library that automatically resizes a text area based on - * its contents. - * - * Requires prototypejs 1.6+. - * - * Usage: - * ------ - * cs = new TextareaResize(id[, options]); - * - * id = (string|Element) DOM ID/Element object of textarea. - * options = (object) Additional options: - * 'max_rows' - (Number) The maximum number of rows to display. - * 'observe_time' - (Number) The interval between form field checks. - * - * Custom Events: - * -------------- - * TexareaResize:resize - * Fired when the textarea is resized. - * params: NONE - * - * @author Michael Slusarz - */ - -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'); - } - } - -}); diff --git a/horde/js/autocomplete.js b/horde/js/autocomplete.js index aaea2d45f..f12471731 100644 --- a/horde/js/autocomplete.js +++ b/horde/js/autocomplete.js @@ -1,7 +1,7 @@ /** * autocomplete.js - A javascript library which implements autocomplete. * Requires prototype.js v1.6.0.2+, scriptaculous v1.8.0+ (effects.js), - * and KeyNavList.js. + * and keynavlist.js. * * Adapted from script.aculo.us controls.js v1.8.0 * (c) 2005-2007 Thomas Fuchs, Ivan Krstic, and Jon Tirsen diff --git a/horde/js/contextsensitive.js b/horde/js/contextsensitive.js new file mode 100644 index 000000000..8c76ae927 --- /dev/null +++ b/horde/js/contextsensitive.js @@ -0,0 +1,388 @@ +/** + * ContextSensitive: a library for generating context-sensitive content on + * HTML elements. It will take over the click/oncontextmenu functions for the + * document, and works only where these are possible to override. It allows + * contextmenus to be created via both a left and right mouse click. + * + * On Opera, the context menu is triggered by a left click + SHIFT + CTRL + * combination. + * + * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only). + * + * + * Usage: + * ------ + * cs = new ContextSensitive(); + * + * Custom Events: + * -------------- + * Custom events are triggered on the base element. The parameters given + * below are available through the 'memo' property of the Event object. + * + * ContextSensitive:click + * Fired when a contextmenu element is clicked on. + * params: (object) elt - (Element) The menu element clicked on. + * trigger - (string) The parent menu. + * + * ContextSensitive:show + * Fired before a contextmenu is displayed. + * params: (string) The DOM ID of the context menu. + * + * + * Original code by Havard Eide (http://eide.org/) released under the MIT + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Chuck Hagenbuch + * @author Michael Slusarz + */ + +var ContextSensitive = Class.create({ + + initialize: function() + { + this.baseelt = null; + this.current = []; + this.elements = $H(); + this.submenus = $H(); + this.triggers = []; + + if (!Prototype.Browser.Opera) { + document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this)); + } + document.observe('click', this._leftClickHandler.bindAsEventListener(this)); + document.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousescroll', this.close.bind(this)); + }, + + /** + * Elements are of type ContextSensitive.Element. + */ + addElement: function(id, target, opts) + { + var left = Boolean(opts.left); + if (id && !this.validElement(id, left)) { + this.elements.set(id + Number(left), new ContextSensitive.Element(id, target, opts)); + } + }, + + /** + * Remove a registered element. + */ + removeElement: function(id) + { + this.elements.unset(id + '0'); + this.elements.unset(id + '1'); + }, + + /** + * Hide the currently displayed element(s). + */ + close: function() + { + this._closeMenu(0, true); + }, + + /** + * Close all menus below a specified level. + */ + _closeMenu: function(idx, immediate) + { + if (this.current.size()) { + this.current.splice(idx, this.current.size() - idx).each(function(s) { + // Fade-out on final display. + if (!immediate && idx == 0) { + s.fade({ duration: 0.15 }); + } else { + $(s).hide(); + } + }); + + this.triggers.splice(idx, this.triggers.size() - idx).each(function(s) { + $(s).removeClassName('contextHover'); + }); + + if (idx == 0) { + this.baseelt = null; + } + } + }, + + /** + * Returns the current displayed menu element ID, if any. If more than one + * submenu is open, returns the last ID opened. + */ + currentmenu: function() + { + return this.current.last(); + }, + + /** + * Get a valid element (the ones that can be right-clicked) based + * on a element ID. + */ + validElement: function(id, left) + { + return this.elements.get(id + Number(Boolean(left))); + }, + + /** + * Set the disabled flag of an event. + */ + disable: function(id, left, disable) + { + var e = this.validElement(id, left); + if (e) { + e.disable = disable; + } + }, + + /** + * Called when a left click event occurs. Will return before the + * element is closed if we click on an element inside of it. + */ + _leftClickHandler: function(e) + { + var base, elt, elt_up, trigger; + + if (this.operaCheck(e)) { + this._rightClickHandler(e, false); + e.stop(); + return; + } + + // Check for a right click. FF on Linux triggers an onclick event even + // w/a right click, so disregard. + if (e.isRightClick()) { + return; + } + + // Check for click in open contextmenu. + if (this.current.size()) { + elt = e.element(); + if (!elt.match('A')) { + elt = elt.up('A'); + if (!elt) { + this._rightClickHandler(e, true); + return; + } + } + elt_up = elt.up('.contextMenu'); + + if (elt_up) { + e.stop(); + + if (elt.hasClassName('contextSubmenu') && + elt_up.identify() != this.currentmenu()) { + this._closeMenu(this.current.indexOf(elt.identify())); + } + + base = this.baseelt; + trigger = this.triggers.last(); + this.close(); + base.fire('ContextSensitive:click', { elt: elt, trigger: trigger }); + return; + } + } + + // Check if the mouseclick is registered to an element now. + this._rightClickHandler(e, true); + }, + + /** + * Checks if the Opera right-click emulation is present. + */ + operaCheck: function(e) + { + return Prototype.Browser.Opera && e.shiftKey && e.ctrlKey; + }, + + /** + * Called when a right click event occurs. + */ + _rightClickHandler: function(e, left) + { + if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) { + e.stop(); + } + }, + + /** + * Display context menu if valid element has been activated. + */ + trigger: function(target, leftclick, x, y) + { + var ctx, el, offset, offsets, voffsets; + + [ target ].concat(target.ancestors()).find(function(n) { + ctx = this.validElement(n.id, leftclick); + return ctx; + }, this); + + // Try to retrieve the context-sensitive element we want to + // display. If we can't find it we just return. + if (!ctx || + ctx.disable || + !(el = $(ctx.ctx)) || + (leftclick && target == this.baseelt) || + this.currentmenu() == ctx.ctx) { + this.close(); + return false; + } + + this.close(); + + // Register the element that was clicked on. + this.baseelt = target; + + offset = ctx.opts.offset; + if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) { + offset = target.identify(); + } + offset = $(offset); + + if (offset) { + offsets = offset.viewportOffset(); + voffsets = document.viewport.getScrollOffsets(); + x = offsets[0] + voffsets.left; + y = offsets[1] + offset.getHeight() + voffsets.top; + } + + this._displayMenu(el, x, y); + this.triggers.push(el.identify()); + + return true; + }, + + /** + * Display the [sub]menu on the screen. + */ + _displayMenu: function(elt, x, y) + { + // Get window/element dimensions + var eltL, h, w, + id = elt.identify(), + v = document.viewport.getDimensions(); + + elt.setStyle({ visibility: 'hidden' }).show(); + eltL = elt.getLayout(), + h = eltL.get('border-box-height'); + w = eltL.get('border-box-width'); + elt.hide().setStyle({ visibility: 'visible' }); + + // Make sure context window is entirely on screen + if ((y + h) > v.height) { + y = v.height - h - 2; + } + + if ((x + w) > v.width) { + x = this.current.size() + ? ($(this.current.last()).viewportOffset()[0] - w) + : (v.width - w - 2); + } + + this.baseelt.fire('ContextSensitive:show', id); + + elt.setStyle({ left: x + 'px', top: y + 'px' }) + + if (this.current.size()) { + elt.show(); + } else { + // Fade-in on initial display. + elt.appear({ duration: 0.15 }); + } + + this.current.push(id); + }, + + /** + * Add a submenu to an existing menu. + */ + addSubMenu: function(id, submenu) + { + if (!this.submenus.get(id)) { + if (!this.submenus.size()) { + document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this)); + } + this.submenus.set(id, submenu); + $(submenu).addClassName('contextMenu'); + $(id).addClassName('contextSubmenu'); + } + }, + + /** + * Mouseover DOM Event handler. + */ + _mouseoverHandler: function(e) + { + if (!this.current.size()) { + return; + } + + var cm = this.currentmenu(), + elt = e.element(), + elt_up = elt.up('.contextMenu'), + id = elt.identify(), + id_div, offsets, sub, voffsets, x, y; + + if (!elt_up) { + return; + } + + id_div = elt_up.identify(); + + if (elt.hasClassName('contextSubmenu')) { + sub = this.submenus.get(id); + if (sub != cm || this.currentmenu() != id) { + if (id_div != cm) { + this._closeMenu(this.current.indexOf(id_div) + 1); + } + + offsets = elt.viewportOffset(); + voffsets = document.viewport.getScrollOffsets(); + x = offsets[0] + voffsets.left + elt.getWidth(); + y = offsets[1] + voffsets.top; + this._displayMenu($(sub), x, y, id); + this.triggers.push(id); + elt.addClassName('contextHover'); + } + } else if ((this.current.size() > 1) && + id_div != cm) { + this._closeMenu(this.current.indexOf(id)); + } + } + +}); + +ContextSensitive.Element = Class.create({ + + // opts: 'left' -> monitor left click; 'offset' -> id of element used to + // determine offset placement + initialize: function(id, target, opts) + { + this.id = id; + this.ctx = target; + this.opts = opts; + this.opts.left = Boolean(opts.left); + this.disable = false; + + target = $(target); + if (target) { + target.addClassName('contextMenu'); + } + } + +}); diff --git a/horde/js/dhtmlHistory.js b/horde/js/dhtmlHistory.js deleted file mode 100644 index 49681aa50..000000000 --- a/horde/js/dhtmlHistory.js +++ /dev/null @@ -1,499 +0,0 @@ -/** - * dhtmlHistory - An object that provides DHTML history, history data, and - * bookmarking for AJAX applications. - * - * Copyright (c) 2007 Brian Dillard and Brad Neuberg: - * Brian Dillard | Project Lead | bdillard@pathf.com | http://blogs.pathf.com/agileajax/ - * Brad Neuberg | Original Project Creator | http://codinginparadise.org - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * This file has been altered from the original dhtmlHistory (v0.05; SVN - * revision 114) to remove unneeded functionality and to provide bug fixes and - * enhancements. - * - * This file requires the Prototype Javscript Library v1.6.0+ - * - * Additions Copyright 2005-2010 The Horde Project (http://www.horde.org/) - */ - -window.Horde = window.Horde || {}; - -Horde.dhtmlHistory = { - /* Our current hash location, without the "#" symbol. */ - // currentLocation: null, - - /* Our history change listener. */ - // listener: null, - - /* A hidden IFrame we use in Internet Explorer to detect history - changes. */ - // iframe: null, - - /* Indicates to the browser whether to ignore location changes. */ - // ignoreLocationChange: null, - - /* The amount of time in milliseconds that we should wait between add - requests. Firefox is okay with 200 ms, but Internet Explorer needs - 400. */ - WAIT_TIME: 200, - - /* The amount of time in milliseconds an add request has to wait in line - before being run on a setTimeout(). */ - currentWaitTime: 0, - - /* A flag that indicates that we should fire a history change event when - we are ready, i.e. after we are initialized and we have a history - change listener. This is needed due to an edge case in browsers other - than Internet Explorer; if you leave a page entirely then return, we - must fire this as a history change event. Unfortunately, we have lost - all references to listeners from earlier, because JavaScript - clears out. */ - // fireOnNewListener: null, - - /* A variable that indicates whether this is the first time this page has - been loaded. If you go to a web page, leave it for another one, and - then return, the page's onload listener fires again. We need a way to - differentiate between the first page load and subsequent ones. This - variable works hand in hand with the pageLoaded variable we store into - historyStorage. */ - // firstLoad: false, - - /* A variable to handle an important edge case in Internet Explorer. In - IE, if a user manually types an address into their browser's location - bar, we must intercept this by continiously checking the location bar - with a timer interval. However, if we manually change the location - bar ourselves programmatically, when using our hidden iframe, we need - to ignore these changes. Unfortunately, these changes are not atomic, - so we surround them with the variable 'ieAtomicLocationChange', that if - true means we are programmatically setting the location and should - ignore this atomic chunked change. */ - // ieAtomicLocationChange: null, - - /* Safari only variables. */ - // safariHistoryStartPoint: null, - // safariStack: null, - - /* PeriodicalExecuter instance. */ - // pe: null, - - /* Initializes our DHTML history. You should call this after the page is - finished loading. Returns true on success, false on failure. */ - initialize: function() - { - if (navigator.vendor && navigator.vendor === 'KDE') { - return false; - } - - Horde.historyStorage.init(); - - if (Prototype.Browser.WebKit) { - this.createSafari(); - } else if (Prototype.Browser.Opera) { - this.createOpera(); - } - - // Get our initial location - this.currentLocation = this.getCurrentLocation(); - - // Write out a hidden iframe for IE and set the amount of time to - // wait between add() requests. - if (Prototype.Browser.IE) { - this.iframe = new Element('IFRAME', { frameborder: 0, name: 'DhtmlHistoryFrame', id: 'DhtmlHistoryFrame', src: 'javascript:false;' }).hide(); - $(document.body).insert(this.iframe); - this.writeIframe(this.currentLocation); - - // Wait 400 milliseconds between history updates on IE - this.WAIT_TIME = 400; - - this.ignoreLocationChange = true; - } - - /* Add an unload listener for the page; this is needed for FF 1.5+ - because this browser caches all dynamic updates to the page, which - can break some of our logic related to testing whether this is the - first instance a page has loaded or whether it is being pulled from - the cache. */ - Event.observe(window, 'unload', function() { this.firstLoad = false; }.bind(this)); - - this.isFirstLoad(); - - /* Other browsers can use a location handler that checks at regular - intervals as their primary mechanism; we use it for IE as well to - handle an important edge case; see checkLocation() for details. */ - this.pe = new PeriodicalExecuter(this.checkLocation.bind(this), 0.1); - - return true; - }, - - stop: function() - { - if (this.pe) { - this.pe.stop(); - } - }, - - /* Adds a history change listener. Note that only one listener is - supported at this time. */ - addListener: function(callback) - { - this.listener = callback; - - /* If the page was just loaded and we should not ignore it, fire an - event to our new listener now. */ - if (this.fireOnNewListener) { - if (this.currentLocation) { - this.fireHistoryEvent(this.currentLocation); - } - this.fireOnNewListener = false; - } - }, - - add: function(newLoc, historyData) - { - if (Prototype.Browser.WebKit) { - newLoc = this.removeHash(newLoc); - Horde.historyStorage.put(newLoc, historyData); - this.currentLocation = newLoc; - this.ignoreLocationChange = true; - this.setLocation(newLoc); - this.putSafariState(newLoc); - } else { - /* Most browsers require that we wait a certain amount of time - before changing the location, such as 200 milliseconds; rather - than forcing external callers to use setTimeout() to account for - this to prevent bugs, we internally handle this detail by using - a 'currentWaitTime' variable and have requests wait in line. */ - setTimeout(this.addImpl.bind(this, newLoc, historyData), this.currentWaitTime); - } - - // Indicate that the next request will have to wait for awhile - this.currentWaitTime += this.WAIT_TIME; - }, - - setLocation: function(loc) - { - location.hash = loc; - }, - - /* Gets the current hash value that is in the browser's location bar, - removing leading # symbols if they are present. */ - getCurrentLocation: function() - { - if (Prototype.Browser.WebKit) { - return this.getSafariState(); - } else { - return this.removeHash(decodeURIComponent(location.hash)); - } - }, - - addImpl: function(newLoc, historyData) - { - // Indicate that the current wait time is now less - if (this.currentWaitTime) { - this.currentWaitTime -= this.WAIT_TIME; - } - - /* IE has a strange bug; if the newLoc is the same as _any_ - preexisting id in the document, then the history action gets - recorded twice; return immediately if there is an element with - this ID. */ - if ($('newLoc')) { - return; - } - - // Remove any leading hash symbols on newLoc - newLoc = this.removeHash(newLoc); - - // Store the history data into history storage - Horde.historyStorage.put(newLoc, historyData); - - // Indicate to the browser to ignore this upcoming location change. - // Indicate to IE that this is an atomic location change block. - this.ignoreLocationChange = this.ieAtomicLocationChange = true; - - // Save this as our current location and change the browser location - this.currentLocation = newLoc; - this.setLocation(encodeURIComponent(newLoc)); - - // Change the hidden iframe's location if on IE - if (Prototype.Browser.IE) { - this.writeIframe(newLoc); - } - - // End of atomic location change block for IE - this.ieAtomicLocationChange = false; - }, - - isFirstLoad: function() - { - if (!Horde.historyStorage.hasKey("DhtmlHistory_pageLoaded")) { - if (Prototype.Browser.IE) { - this.fireOnNewListener = false; - } else { - this.ignoreLocationChange = true; - } - this.firstLoad = true; - Horde.historyStorage.put("DhtmlHistory_pageLoaded", true); - } else { - if (Prototype.Browser.IE) { - this.firstLoad = false; - } else { - /* Indicate that we want to pay attention to this location - change. */ - this.ignoreLocationChange = false; - } - - /* For browsers other than IE, fire a history change event; - on IE, the event will be thrown automatically when it's - hidden iframe reloads on page load. Unfortunately, we don't - have any listeners yet; indicate that we want to fire an - event when a listener is added. */ - this.fireOnNewListener = true; - } - }, - - /* Notify the listener of new history changes. */ - fireHistoryEvent: function(newHash) - { - if (this.listener) { - // Extract the value from our history storage for this hash and - // call our listener. - this.listener.call(null, newHash, Horde.historyStorage.get(newHash)); - } - }, - - /* See if the browser has changed location. This is the primary history - mechanism for FF. For IE, we use this to handle an important edge case: - if a user manually types in a new hash value into their IE location - bar and press enter, we want to intercept this and notify any history - listener. */ - checkLocation: function() - { - /* Ignore any location changes that we made ourselves for browsers - other than IE. */ - if (!Prototype.Browser.IE) { - if (this.ignoreLocationChange) { - this.ignoreLocationChange = false; - return; - } - } else if (this.ieAtomicLocationChange) { - /* If we are dealing with IE and we are in the middle of making a - location change from an iframe, ignore it. */ - return; - } - - // Get hash location - var hash = this.getCurrentLocation(); - - // See if there has been a change or there is no hash location - if (hash.replace(/@/, '%40') == this.currentLocation || Object.isUndefined(hash)) { - return; - } - - /* On IE, we need to intercept users manually entering locations into - the browser; we do this by comparing the browsers location against - the iframes location; if they differ, we are dealing with a manual - event and need to place it inside our history, otherwise we can - return. */ - this.ieAtomicLocationChange = true; - - if (Prototype.Browser.IE) { - if (this.iframe.contentWindow.l == hash) { - // The iframe is unchanged - return; - } - this.writeIframe(hash); - } - - // Save this new location - this.currentLocation = hash; - - this.ieAtomicLocationChange = false; - - // Notify listeners of the change - this.fireHistoryEvent(hash); - }, - - /* Removes any leading hash that might be on a location. */ - removeHash: function(h) - { - if (h === null || Object.isUndefined(h)) { - return null; - } else if (h.startsWith('#')) { - if (h.length == 1) { - return ""; - } else { - return h.substring(1); - } - } - return h; - }, - - // IE Specific Code - /* For IE, says when the hidden iframe has finished loading. */ - iframeLoaded: function(newLoc) - { - // Ignore any location changes that we made ourselves - if (this.ignoreLocationChange) { - this.ignoreLocationChange = false; - return; - } - - // Get the new location - this.setLocation(encodeURIComponent(newLoc)); - - // Notify listeners of the change - this.fireHistoryEvent(newLoc); - }, - - writeIframe: function(l) - { - var d = this.iframe.contentWindow.document; - d.open(); - d.write(''); - d.close(); - }, - - // Safari specific code - createSafari: function() - { - this.WAIT_TIME = 400; - this.safariHistoryStartPoint = history.length; - - this.safariStack = new Element('INPUT', { id: 'DhtmlSafariHistory', type: 'text', value: '[]' }).hide(); - $(document.body).insert(this.safariStack); - }, - - getSafariStack: function() - { - return $F(this.safariStack).evalJSON(); - }, - - getSafariState: function() - { - var stack = this.getSafariStack(); - return stack[history.length - this.safariHistoryStartPoint - 1]; - }, - - putSafariState: function(newLoc) - { - var stack = this.getSafariStack(); - stack[history.length - this.safariHistoryStartPoint] = newLoc; - this.safariStack.setValue(stack.toJSON()); - }, - - // Opera specific code - createOpera: function() - { - this.WAIT_TIME = 400; - $(document.body).insert(new Element('IMG', { src: "javascript:location.href='javascript:Horde.dhtmlHistory.checkLocation();'" }).hide()); - } -}; - -/* An object that uses a hidden form to store history state across page loads. - The chief mechanism for doing so is using the fact that browsers save the - text in form data for the life of the browser and cache, which means the - text is still there when the user navigates back to the page. See - http://codinginparadise.org/weblog/2005/08/ajax-tutorial-saving-session-across.html - for full details. */ -Horde.historyStorage = { - /* Our hash of key name/values. */ - // storageHash: null, - - /* A reference to our textarea field. */ - // storageField: null, - - put: function(key, value) - { - this.loadHashTable(); - - // Store this new key - this.storageHash.set(key, value); - - // Save and serialize the hashtable into the form - this.saveHashTable(); - }, - - get: function(key) - { - // Make sure the hash table has been loaded from the form - this.loadHashTable(); - - var value = this.storageHash.get(key); - return Object.isUndefined(value) ? null : value; - }, - - remove: function(key) - { - // Make sure the hash table has been loaded from the form - this.loadHashTable(); - - // Delete the value - this.storageHash.unset(key); - - // Serialize and save the hash table into the form - this.saveHashTable(); - }, - - /* Clears out all saved data. */ - reset: function() - { - this.storageField.value = ""; - this.storageHash = $H(); - }, - - hasKey: function(key) - { - // Make sure the hash table has been loaded from the form - this.loadHashTable(); - return !(typeof this.storageHash.get(key) == undefined); - }, - - init: function() - { - // Write a hidden form into the page - var form = new Element('FORM').hide(); - $(document.body).insert(form); - - this.storageField = new Element('TEXTAREA', { id: 'historyStorageField' }); - form.insert(this.storageField); - - if (Prototype.Browser.Opera) { - this.storageField.focus(); - } - }, - - /* Loads the hash table up from the form. */ - loadHashTable: function() - { - if (!this.storageHash) { - // Destringify the content back into a real JS object - this.storageHash = (this.storageField.value) ? this.storageField.value.evalJSON() : $H(); - } - }, - - /* Saves the hash table into the form. */ - saveHashTable: function() - { - this.loadHashTable(); - this.storageField.value = this.storageHash.toJSON(); - } - -}; diff --git a/horde/js/dhtmlhistory.js b/horde/js/dhtmlhistory.js new file mode 100644 index 000000000..49681aa50 --- /dev/null +++ b/horde/js/dhtmlhistory.js @@ -0,0 +1,499 @@ +/** + * dhtmlHistory - An object that provides DHTML history, history data, and + * bookmarking for AJAX applications. + * + * Copyright (c) 2007 Brian Dillard and Brad Neuberg: + * Brian Dillard | Project Lead | bdillard@pathf.com | http://blogs.pathf.com/agileajax/ + * Brad Neuberg | Original Project Creator | http://codinginparadise.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * This file has been altered from the original dhtmlHistory (v0.05; SVN + * revision 114) to remove unneeded functionality and to provide bug fixes and + * enhancements. + * + * This file requires the Prototype Javscript Library v1.6.0+ + * + * Additions Copyright 2005-2010 The Horde Project (http://www.horde.org/) + */ + +window.Horde = window.Horde || {}; + +Horde.dhtmlHistory = { + /* Our current hash location, without the "#" symbol. */ + // currentLocation: null, + + /* Our history change listener. */ + // listener: null, + + /* A hidden IFrame we use in Internet Explorer to detect history + changes. */ + // iframe: null, + + /* Indicates to the browser whether to ignore location changes. */ + // ignoreLocationChange: null, + + /* The amount of time in milliseconds that we should wait between add + requests. Firefox is okay with 200 ms, but Internet Explorer needs + 400. */ + WAIT_TIME: 200, + + /* The amount of time in milliseconds an add request has to wait in line + before being run on a setTimeout(). */ + currentWaitTime: 0, + + /* A flag that indicates that we should fire a history change event when + we are ready, i.e. after we are initialized and we have a history + change listener. This is needed due to an edge case in browsers other + than Internet Explorer; if you leave a page entirely then return, we + must fire this as a history change event. Unfortunately, we have lost + all references to listeners from earlier, because JavaScript + clears out. */ + // fireOnNewListener: null, + + /* A variable that indicates whether this is the first time this page has + been loaded. If you go to a web page, leave it for another one, and + then return, the page's onload listener fires again. We need a way to + differentiate between the first page load and subsequent ones. This + variable works hand in hand with the pageLoaded variable we store into + historyStorage. */ + // firstLoad: false, + + /* A variable to handle an important edge case in Internet Explorer. In + IE, if a user manually types an address into their browser's location + bar, we must intercept this by continiously checking the location bar + with a timer interval. However, if we manually change the location + bar ourselves programmatically, when using our hidden iframe, we need + to ignore these changes. Unfortunately, these changes are not atomic, + so we surround them with the variable 'ieAtomicLocationChange', that if + true means we are programmatically setting the location and should + ignore this atomic chunked change. */ + // ieAtomicLocationChange: null, + + /* Safari only variables. */ + // safariHistoryStartPoint: null, + // safariStack: null, + + /* PeriodicalExecuter instance. */ + // pe: null, + + /* Initializes our DHTML history. You should call this after the page is + finished loading. Returns true on success, false on failure. */ + initialize: function() + { + if (navigator.vendor && navigator.vendor === 'KDE') { + return false; + } + + Horde.historyStorage.init(); + + if (Prototype.Browser.WebKit) { + this.createSafari(); + } else if (Prototype.Browser.Opera) { + this.createOpera(); + } + + // Get our initial location + this.currentLocation = this.getCurrentLocation(); + + // Write out a hidden iframe for IE and set the amount of time to + // wait between add() requests. + if (Prototype.Browser.IE) { + this.iframe = new Element('IFRAME', { frameborder: 0, name: 'DhtmlHistoryFrame', id: 'DhtmlHistoryFrame', src: 'javascript:false;' }).hide(); + $(document.body).insert(this.iframe); + this.writeIframe(this.currentLocation); + + // Wait 400 milliseconds between history updates on IE + this.WAIT_TIME = 400; + + this.ignoreLocationChange = true; + } + + /* Add an unload listener for the page; this is needed for FF 1.5+ + because this browser caches all dynamic updates to the page, which + can break some of our logic related to testing whether this is the + first instance a page has loaded or whether it is being pulled from + the cache. */ + Event.observe(window, 'unload', function() { this.firstLoad = false; }.bind(this)); + + this.isFirstLoad(); + + /* Other browsers can use a location handler that checks at regular + intervals as their primary mechanism; we use it for IE as well to + handle an important edge case; see checkLocation() for details. */ + this.pe = new PeriodicalExecuter(this.checkLocation.bind(this), 0.1); + + return true; + }, + + stop: function() + { + if (this.pe) { + this.pe.stop(); + } + }, + + /* Adds a history change listener. Note that only one listener is + supported at this time. */ + addListener: function(callback) + { + this.listener = callback; + + /* If the page was just loaded and we should not ignore it, fire an + event to our new listener now. */ + if (this.fireOnNewListener) { + if (this.currentLocation) { + this.fireHistoryEvent(this.currentLocation); + } + this.fireOnNewListener = false; + } + }, + + add: function(newLoc, historyData) + { + if (Prototype.Browser.WebKit) { + newLoc = this.removeHash(newLoc); + Horde.historyStorage.put(newLoc, historyData); + this.currentLocation = newLoc; + this.ignoreLocationChange = true; + this.setLocation(newLoc); + this.putSafariState(newLoc); + } else { + /* Most browsers require that we wait a certain amount of time + before changing the location, such as 200 milliseconds; rather + than forcing external callers to use setTimeout() to account for + this to prevent bugs, we internally handle this detail by using + a 'currentWaitTime' variable and have requests wait in line. */ + setTimeout(this.addImpl.bind(this, newLoc, historyData), this.currentWaitTime); + } + + // Indicate that the next request will have to wait for awhile + this.currentWaitTime += this.WAIT_TIME; + }, + + setLocation: function(loc) + { + location.hash = loc; + }, + + /* Gets the current hash value that is in the browser's location bar, + removing leading # symbols if they are present. */ + getCurrentLocation: function() + { + if (Prototype.Browser.WebKit) { + return this.getSafariState(); + } else { + return this.removeHash(decodeURIComponent(location.hash)); + } + }, + + addImpl: function(newLoc, historyData) + { + // Indicate that the current wait time is now less + if (this.currentWaitTime) { + this.currentWaitTime -= this.WAIT_TIME; + } + + /* IE has a strange bug; if the newLoc is the same as _any_ + preexisting id in the document, then the history action gets + recorded twice; return immediately if there is an element with + this ID. */ + if ($('newLoc')) { + return; + } + + // Remove any leading hash symbols on newLoc + newLoc = this.removeHash(newLoc); + + // Store the history data into history storage + Horde.historyStorage.put(newLoc, historyData); + + // Indicate to the browser to ignore this upcoming location change. + // Indicate to IE that this is an atomic location change block. + this.ignoreLocationChange = this.ieAtomicLocationChange = true; + + // Save this as our current location and change the browser location + this.currentLocation = newLoc; + this.setLocation(encodeURIComponent(newLoc)); + + // Change the hidden iframe's location if on IE + if (Prototype.Browser.IE) { + this.writeIframe(newLoc); + } + + // End of atomic location change block for IE + this.ieAtomicLocationChange = false; + }, + + isFirstLoad: function() + { + if (!Horde.historyStorage.hasKey("DhtmlHistory_pageLoaded")) { + if (Prototype.Browser.IE) { + this.fireOnNewListener = false; + } else { + this.ignoreLocationChange = true; + } + this.firstLoad = true; + Horde.historyStorage.put("DhtmlHistory_pageLoaded", true); + } else { + if (Prototype.Browser.IE) { + this.firstLoad = false; + } else { + /* Indicate that we want to pay attention to this location + change. */ + this.ignoreLocationChange = false; + } + + /* For browsers other than IE, fire a history change event; + on IE, the event will be thrown automatically when it's + hidden iframe reloads on page load. Unfortunately, we don't + have any listeners yet; indicate that we want to fire an + event when a listener is added. */ + this.fireOnNewListener = true; + } + }, + + /* Notify the listener of new history changes. */ + fireHistoryEvent: function(newHash) + { + if (this.listener) { + // Extract the value from our history storage for this hash and + // call our listener. + this.listener.call(null, newHash, Horde.historyStorage.get(newHash)); + } + }, + + /* See if the browser has changed location. This is the primary history + mechanism for FF. For IE, we use this to handle an important edge case: + if a user manually types in a new hash value into their IE location + bar and press enter, we want to intercept this and notify any history + listener. */ + checkLocation: function() + { + /* Ignore any location changes that we made ourselves for browsers + other than IE. */ + if (!Prototype.Browser.IE) { + if (this.ignoreLocationChange) { + this.ignoreLocationChange = false; + return; + } + } else if (this.ieAtomicLocationChange) { + /* If we are dealing with IE and we are in the middle of making a + location change from an iframe, ignore it. */ + return; + } + + // Get hash location + var hash = this.getCurrentLocation(); + + // See if there has been a change or there is no hash location + if (hash.replace(/@/, '%40') == this.currentLocation || Object.isUndefined(hash)) { + return; + } + + /* On IE, we need to intercept users manually entering locations into + the browser; we do this by comparing the browsers location against + the iframes location; if they differ, we are dealing with a manual + event and need to place it inside our history, otherwise we can + return. */ + this.ieAtomicLocationChange = true; + + if (Prototype.Browser.IE) { + if (this.iframe.contentWindow.l == hash) { + // The iframe is unchanged + return; + } + this.writeIframe(hash); + } + + // Save this new location + this.currentLocation = hash; + + this.ieAtomicLocationChange = false; + + // Notify listeners of the change + this.fireHistoryEvent(hash); + }, + + /* Removes any leading hash that might be on a location. */ + removeHash: function(h) + { + if (h === null || Object.isUndefined(h)) { + return null; + } else if (h.startsWith('#')) { + if (h.length == 1) { + return ""; + } else { + return h.substring(1); + } + } + return h; + }, + + // IE Specific Code + /* For IE, says when the hidden iframe has finished loading. */ + iframeLoaded: function(newLoc) + { + // Ignore any location changes that we made ourselves + if (this.ignoreLocationChange) { + this.ignoreLocationChange = false; + return; + } + + // Get the new location + this.setLocation(encodeURIComponent(newLoc)); + + // Notify listeners of the change + this.fireHistoryEvent(newLoc); + }, + + writeIframe: function(l) + { + var d = this.iframe.contentWindow.document; + d.open(); + d.write(''); + d.close(); + }, + + // Safari specific code + createSafari: function() + { + this.WAIT_TIME = 400; + this.safariHistoryStartPoint = history.length; + + this.safariStack = new Element('INPUT', { id: 'DhtmlSafariHistory', type: 'text', value: '[]' }).hide(); + $(document.body).insert(this.safariStack); + }, + + getSafariStack: function() + { + return $F(this.safariStack).evalJSON(); + }, + + getSafariState: function() + { + var stack = this.getSafariStack(); + return stack[history.length - this.safariHistoryStartPoint - 1]; + }, + + putSafariState: function(newLoc) + { + var stack = this.getSafariStack(); + stack[history.length - this.safariHistoryStartPoint] = newLoc; + this.safariStack.setValue(stack.toJSON()); + }, + + // Opera specific code + createOpera: function() + { + this.WAIT_TIME = 400; + $(document.body).insert(new Element('IMG', { src: "javascript:location.href='javascript:Horde.dhtmlHistory.checkLocation();'" }).hide()); + } +}; + +/* An object that uses a hidden form to store history state across page loads. + The chief mechanism for doing so is using the fact that browsers save the + text in form data for the life of the browser and cache, which means the + text is still there when the user navigates back to the page. See + http://codinginparadise.org/weblog/2005/08/ajax-tutorial-saving-session-across.html + for full details. */ +Horde.historyStorage = { + /* Our hash of key name/values. */ + // storageHash: null, + + /* A reference to our textarea field. */ + // storageField: null, + + put: function(key, value) + { + this.loadHashTable(); + + // Store this new key + this.storageHash.set(key, value); + + // Save and serialize the hashtable into the form + this.saveHashTable(); + }, + + get: function(key) + { + // Make sure the hash table has been loaded from the form + this.loadHashTable(); + + var value = this.storageHash.get(key); + return Object.isUndefined(value) ? null : value; + }, + + remove: function(key) + { + // Make sure the hash table has been loaded from the form + this.loadHashTable(); + + // Delete the value + this.storageHash.unset(key); + + // Serialize and save the hash table into the form + this.saveHashTable(); + }, + + /* Clears out all saved data. */ + reset: function() + { + this.storageField.value = ""; + this.storageHash = $H(); + }, + + hasKey: function(key) + { + // Make sure the hash table has been loaded from the form + this.loadHashTable(); + return !(typeof this.storageHash.get(key) == undefined); + }, + + init: function() + { + // Write a hidden form into the page + var form = new Element('FORM').hide(); + $(document.body).insert(form); + + this.storageField = new Element('TEXTAREA', { id: 'historyStorageField' }); + form.insert(this.storageField); + + if (Prototype.Browser.Opera) { + this.storageField.focus(); + } + }, + + /* Loads the hash table up from the form. */ + loadHashTable: function() + { + if (!this.storageHash) { + // Destringify the content back into a real JS object + this.storageHash = (this.storageField.value) ? this.storageField.value.evalJSON() : $H(); + } + }, + + /* Saves the hash table into the form. */ + saveHashTable: function() + { + this.loadHashTable(); + this.storageField.value = this.storageHash.toJSON(); + } + +}; diff --git a/horde/js/growler.js b/horde/js/growler.js new file mode 100644 index 000000000..02ace41cc --- /dev/null +++ b/horde/js/growler.js @@ -0,0 +1,283 @@ +/** + * Growler.js - Display 'Growl'-like notifications. + * + * Notice Options (passed to 'Growler.growl()'): + * 'className' - (string) An optional additional CSS class to apply to notice. + * 'header' - (string) The title that is displayed for the notice. + * 'life' - (float) The number of seconds in which the notice remains visible. + * 'log' - (boolean) If true, will log the entry. + * 'speedin' - (float) The speed in seconds in which the notice is shown. + * 'speedout' - (float) The speed in seconds in which the notice is hidden. + * 'sticky' - (boolean) Determines if the notice should always remain visible + * until closed by the user. + * + * Growler Options (passed to 'new Growler()'): + * 'location' - (string) The location of the growler notices. This can be: + * tr (top-right) + * br (bottom-right) + * tl (top-left) + * bl (bottom-left) + * tc (top-center) + * bc (bottom-center) + * 'log' - (boolean) Enable logging. + * 'noalerts' - (string) The localized string to display when no log entries + * are present. + * + * Custom Events: + * -------------- + * Custom events are triggered on the notice element. The parameters given + * below are available through the 'memo' property of the Event object. + * + * Growler:created + * Fired on TODO + * params: NONE + * + * Growler:destroyed + * Fired on TODO + * params: NONE + * + * + * Growler has been tested with Safari 3(Mac|Win), Firefox 3(Mac|Win), IE6, + * IE7, and Opera. + * + * Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only). + * + * Code adapted from k.Growler v1.0.0 + * http://code.google.com/p/kproto/ + * Written by Kevin Armstrong + * Released under the MIT license + * + * @author Michael Slusarz + */ + +(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(); + } + + }); + +})(); diff --git a/horde/js/ieEscGuard.js b/horde/js/ieEscGuard.js deleted file mode 100644 index 3fa5ad2de..000000000 --- a/horde/js/ieEscGuard.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Javascript code for attaching an onkeydown listener to textarea and - * text input elements to prevent loss of data when the user hits the - * ESC key. - * - * Requires prototypejs 1.6.0.2+. - * - * See the enclosed file COPYING for license information (LGPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. - */ - -/* Finds all text inputs (input type="text") and textarea tags, and - * attaches the onkeydown listener to them to avoid ESC clearing the - * text. */ -if (Prototype.Browser.IE) { - document.observe('dom:loaded', function() { - [ $$('TEXTAREA'), $$('INPUT[type="text"]') ].flatten().each(function(t) { - t.observe('keydown', function(e) { return e.keyCode != 27; }); - }); - }); -} diff --git a/horde/js/ieescguard.js b/horde/js/ieescguard.js new file mode 100644 index 000000000..3fa5ad2de --- /dev/null +++ b/horde/js/ieescguard.js @@ -0,0 +1,21 @@ +/** + * Javascript code for attaching an onkeydown listener to textarea and + * text input elements to prevent loss of data when the user hits the + * ESC key. + * + * Requires prototypejs 1.6.0.2+. + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + */ + +/* Finds all text inputs (input type="text") and textarea tags, and + * attaches the onkeydown listener to them to avoid ESC clearing the + * text. */ +if (Prototype.Browser.IE) { + document.observe('dom:loaded', function() { + [ $$('TEXTAREA'), $$('INPUT[type="text"]') ].flatten().each(function(t) { + t.observe('keydown', function(e) { return e.keyCode != 27; }); + }); + }); +} diff --git a/horde/js/keynavlist.js b/horde/js/keynavlist.js new file mode 100644 index 000000000..58c4c276c --- /dev/null +++ b/horde/js/keynavlist.js @@ -0,0 +1,288 @@ +/** + * Reuseable keyboard or mouse driven list component. Based on + * Scriptaculous' AutoCompleter. + * + * Requires: prototype.js (v1.6.1+) + * + * Usage: + * ====== + * var knl = new KeyNavList(base, { + * 'esc' - (boolean) Escape the displayed output? + * 'list' - (array) Array of objects with the following keys: + * 'l' - (label) Display data + * 's' - (selected) True if this entry should be selected + * by default + * 'v' - (value) Value of entry + * 'onChoose' - (function) Called when an entry is selected. Passed the + * entry value. + * 'onHide' - (function) Called when the list is hidden. Passed the + * list container element. + * 'onShow' - (function) Called when the list is shown. Passed the + * list container element. + * 'domParent' - (Element) Specifies the parent element. Defaults to + * document.body + * 'keydownObserver - (Element) The element to register the keydown handler + * on. Defaults to document. + * }); + * + * [base = (Element) The element to use for display positioning purposes] + * + * Copyright 2005-2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + */ + +var KeyNavList = Class.create({ + + // Vars used: active, div, iefix, ignore, onClickFunc, onKeyDownFunc, + // resizeFunc, selected + + initialize: function(base, opts) + { + this.active = false; + this.base = $(base); + + this.opts = Object.extend({ + onChoose: Prototype.emptyFunction, + onHide: Prototype.emptyFunction, + onShow: Prototype.emptyFunction, + domParent: null, + keydownObserver: document + }, opts || {}); + + this.div = new Element('DIV', { className: 'KeyNavList' }).hide().insert(new Element('UL')); + + if (!this.opts.domParent) { + this.opts.domParent = document.body; + } + this.opts.domParent = $(this.opts.domParent); + + this.opts.domParent.insert(this.div); + + if (this.opts.list) { + this.update(this.opts.list); + delete this.opts.list; + } + + this.onClickFunc = this.onClick.bindAsEventListener(this); + document.observe('click', this.onClickFunc); + + this.onKeyDownFunc = this.onKeyDown.bindAsEventListener(this); + $(this.opts.keydownObserver).observe('keydown', this.onKeyDownFunc); + + this.resizeFunc = this.hide.bind(this); + Event.observe(window, 'resize', this.resizeFunc); + + if (Prototype.Browser.IE && !window.XMLHttpRequest) { + this.iefix = $('knl_iframe_iefix'); + if (!this.iefix) { + this.iefix = new Element('IFRAME', { id: 'knl_iframe_iefix', src: 'javascript:false;', scrolling: 'no', frameborder: 0 }).setStyle({ position: 'absolute', display: 'block', zIndex: 99 }).hide(); + document.body.appendChild(this.iefix); + } + this.div.setStyle({ zIndex: 100 }); + } + }, + + destroy: function() + { + document.stopObserving('click', this.onClickFunc); + document.stopObserving('keydown', this.onKeyDownFunc); + Event.stopObserving(window, 'resize', this.resizeFunc); + this.div.remove(); + if (this.iefix) { + this.iefix.remove(); + } + }, + + update: function(list) + { + var df = document.createDocumentFragment(); + + list.each(function(v) { + var li; + if (this.opts.esc) { + v.l = v.l.escapeHTML().gsub(' ', '  '); + } + li = new Element('LI').insert(v.l).store('v', v.v); + if (v.s) { + this.markSelected(li); + } + df.appendChild(li); + }.bind(this)); + + this.div.down().childElements().invoke('remove'); + this.div.down().appendChild(df); + }, + + updateBase: function(base) + { + this.base = $(base); + }, + + show: function(list) + { + this.active = true; + + if (!Object.isUndefined(list)) { + this.update(list); + } else if (this.div.visible()) { + return; + } + + this.opts.onShow(this.div); + + this.div.setStyle({ height: null, width: null, top: null }).clonePosition(this.base, { + setHeight: false, + setWidth: false, + offsetTop: this.base.getHeight() + }); + + if (this.div.visible()) { + this._sizeDiv(); + } else { + this.div.appear({ + afterFinish: function() { + if (this.selected) { + this.div.scrollTop = this.selected.offsetTop; + } + }.bind(this), + afterSetup: this._sizeDiv.bind(this), + duration: 0.15 + }); + } + }, + + _sizeDiv: function() + { + var divL = this.div.getLayout(), + dl = divL.get('left'), + dt = divL.get('top'), + off = this.opts.domParent.cumulativeOffset(), + v = document.viewport.getDimensions(); + + if ((divL.get('border-box-height') + dt + off.top + 10) > v.height) { + this.div.setStyle({ + height: (v.height - dt - off.top - 10) + 'px', + width: (this.div.scrollWidth + 5) + 'px' + }); + } + + /* Need to do width second - horizontal scrolling might add scroll + * bar. */ + if ((divL.get('border-box-width') + dl + off.left + 5) > v.width) { + dl = (v.width - divL.get('border-box-width') - off.left - 5); + this.div.setStyle({ left: dl + 'px' }); + } + + if (this.iefix) { + this.iefix.clonePosition(this.div); + } + }, + + hide: function() + { + if (this.div.visible()) { + this.active = false; + this.opts.onHide(this.div); + this.div.fade({ duration: 0.15 }); + if (this.iefix) { + this.iefix.hide(); + } + } + }, + + onKeyDown: function(e) + { + if (!this.active) { + return; + } + + switch (e.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.opts.onChoose(this.getCurrentEntry()); + this.hide(); + e.stop(); + return; + + case Event.KEY_ESC: + this.hide(); + e.stop(); + return; + + case Event.KEY_UP: + this.markPrevious(); + e.stop(); + return; + + case Event.KEY_DOWN: + this.markNext(); + e.stop(); + return; + } + }, + + onClick: function(e) + { + if (this.active && this.ignore != e) { + var elt = e.findElement('LI'); + + if (elt && + (elt == this.div || elt.descendantOf(this.div))) { + this.markSelected(elt); + this.opts.onChoose(this.getCurrentEntry()); + e.stop(); + } + this.hide(); + } + + this.ignore = null; + }, + + ignoreClick: function(e) + { + this.ignore = e; + }, + + setSelected: function(value) + { + this.markSelected(this.div.down().childElements().find(function(e) { + return e.retrieve('v') == value; + })); + }, + + markSelected: function(elt) + { + if (this.selected) { + this.selected.removeClassName('selected'); + } + this.selected = elt + ? elt.addClassName('selected') + : null; + }, + + markPrevious: function() + { + this.markSelected(this.selected ? this.selected.previous() : null); + }, + + markNext: function() + { + var elt = this.selected + ? this.selected.next() + : this.div.down().childElements().first(); + + if (elt) { + this.markSelected(elt); + } + }, + + getCurrentEntry: function() + { + return this.selected + ? this.selected.retrieve('v') + : null; + } + +}); diff --git a/horde/js/quickfinder.js b/horde/js/quickfinder.js new file mode 100644 index 000000000..02332528f --- /dev/null +++ b/horde/js/quickfinder.js @@ -0,0 +1,99 @@ +/** + * Component for filtering a table or any list of children based on + * the dynamic value of a text input. It requires the prototype.js + * library. + * + * You should define the CSS class .QuickFinderNoMatch to say what + * happens to items that don't match the criteria. A reasonable + * default would be display:none. + * + * This code is heavily inspired by Filterlicious by Gavin + * Kistner. The filterlicious JavaScript file did not have a license; + * however, most of Gavin's code is under the license defined by + * http://phrogz.net/JS/_ReuseLicense.txt, so I'm including that URL + * and Gavin's name as acknowledgements. + * + * @author Chuck Hagenbuch + */ + +var QuickFinder = { + + attachBehavior: function(input) { + var filterTarget = input.readAttribute('for'); + if (!filterTarget) { + return; + } + + if (filterTarget.indexOf(',') != -1) { + input.filterTargets = []; + var targets = filterTarget.split(','); + for (var i = 0; i < targets.length; ++i) { + var t = $(targets[i]); + if (t) { + input.filterTargets.push(t); + } + } + if (!input.filterTargets.size()) { + return; + } + } else { + input.filterTargets = [ $(filterTarget) ]; + if (!input.filterTargets[0]) { + return; + } + } + + var filterEmpty = input.readAttribute('empty'); + if (filterEmpty) { + input.filterEmpty = $(filterEmpty); + } + + input.observe('keyup', this.onKeyUp.bindAsEventListener(this)); + + for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) { + input.filterTargets[i].childElements().each(function(line) { + var filterText = line.filterText || line.readAttribute('filterText'); + if (!filterText) { + line.filterText = line.innerHTML.stripTags(); + } + line.filterText = line.filterText.toLowerCase(); + }); + } + + this.filter(input); + }, + + onKeyUp: function(e) { + var input = e.element(); + if (input.filterTargets) { + this.filter(input); + } + }, + + filter: function(input) { + var matched = 0, + val = input.value.toLowerCase(); + for (var i = 0, i_max = input.filterTargets.length; i < i_max; i++) { + input.filterTargets[i].childElements().each(function(line) { + var filterText = line.filterText; + if (filterText.indexOf(val) == -1) { + line.addClassName('QuickFinderNoMatch'); + } else { + ++matched; + line.removeClassName('QuickFinderNoMatch'); + } + }); + } + + try { + if (input.filterEmpty) { + (matched == 0) ? input.filterEmpty.show() : input.filterEmpty.hide(); + } + } catch (e) {} + } + +} + +document.observe('dom:loaded', function() { + $$('input').each(QuickFinder.attachBehavior.bind(QuickFinder)); +}); diff --git a/horde/js/textarearesize.js b/horde/js/textarearesize.js new file mode 100644 index 000000000..0946f7e88 --- /dev/null +++ b/horde/js/textarearesize.js @@ -0,0 +1,76 @@ +/** + * TextareaResize: a library that automatically resizes a text area based on + * its contents. + * + * Requires prototypejs 1.6+. + * + * Usage: + * ------ + * cs = new TextareaResize(id[, options]); + * + * id = (string|Element) DOM ID/Element object of textarea. + * options = (object) Additional options: + * 'max_rows' - (Number) The maximum number of rows to display. + * 'observe_time' - (Number) The interval between form field checks. + * + * Custom Events: + * -------------- + * TexareaResize:resize + * Fired when the textarea is resized. + * params: NONE + * + * @author Michael Slusarz + */ + +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'); + } + } + +}); diff --git a/imp/compose-dimp.php b/imp/compose-dimp.php index 093df5309..7f64324e6 100644 --- a/imp/compose-dimp.php +++ b/imp/compose-dimp.php @@ -211,7 +211,7 @@ $scripts = array( array('compose-base.js', 'imp'), array('compose-dimp.js', 'imp'), array('md5.js', 'horde'), - array('TextareaResize.js', 'horde') + array('textarearesize.js', 'horde') ); if (!($prefs->isLocked('default_encrypt')) && diff --git a/imp/compose.php b/imp/compose.php index 6377d48c3..4d32807fe 100644 --- a/imp/compose.php +++ b/imp/compose.php @@ -579,7 +579,7 @@ if ($browser->hasFeature('javascript')) { Horde::logMessage($e, 'ERR'); } } - Horde::addScriptFile('ieEscGuard.js', 'horde'); + Horde::addScriptFile('ieescguard.js', 'horde'); } } diff --git a/imp/index-dimp.php b/imp/index-dimp.php index 826968257..3ad9fe60a 100644 --- a/imp/index-dimp.php +++ b/imp/index-dimp.php @@ -18,12 +18,12 @@ require_once dirname(__FILE__) . '/lib/Application.php'; Horde_Registry::appInit('imp', array('impmode' => 'dimp')); $scripts = array( - array('DimpBase.js', 'imp'), - array('ViewPort.js', 'imp'), + array('dimpbase.js', 'imp'), + array('viewport.js', 'imp'), array('dialog.js', 'imp'), array('mailbox-dimp.js', 'imp'), array('imp.js', 'imp'), - array('ContextSensitive.js', 'horde'), + array('contextsensitive.js', 'horde'), array('dragdrop2.js', 'horde'), array('popup.js', 'horde'), array('redbox.js', 'horde'), diff --git a/imp/js/DimpBase.js b/imp/js/DimpBase.js deleted file mode 100644 index e640987a9..000000000 --- a/imp/js/DimpBase.js +++ /dev/null @@ -1,3143 +0,0 @@ -/** - * DimpBase.js - Javascript used in the base DIMP page. - * - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * See the enclosed file COPYING for license information (GPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. - */ - -var DimpBase = { - // Vars used and defaulting to null/false: - // cfolderaction, folder, folderswitch, pollPE, pp, preview_replace, - // resize, rownum, search, splitbar, template, uid, viewport - // msglist_template_horiz and msglist_template_vert set via - // js/mailbox-dimp.js - cacheids: {}, - lastrow: -1, - pivotrow: -1, - ppcache: {}, - ppfifo: [], - showunsub: 0, - tcache: {}, - - // Preview pane cache size is 20 entries. Given that a reasonable guess - // of an average e-mail size is 10 KB (including headers), also make - // an estimate that the JSON data size will be approx. 10 KB. 200 KB - // should be a fairly safe caching value for any recent browser. - ppcachesize: 20, - - // Message selection functions - - // id = (string) DOM ID - // opts = (Object) Boolean options [ctrl, right, shift] - msgSelect: function(id, opts) - { - var bounds, - row = this.viewport.createSelection('domid', id), - rownum = row.get('rownum').first(), - sel = this.isSelected('domid', id), - selcount = this.selectedCount(); - - this.lastrow = rownum; - - // Some browsers need to stop the mousedown event before it propogates - // down to the browser level in order to prevent text selection on - // drag/drop actions. Clicking on a message should always lose focus - // from the search input, because the user may immediately start - // keyboard navigation after that. Thus, we need to ensure that a - // message click loses focus on the search input. - if ($('qsearch')) { - $('qsearch_input').blur(); - } - - if (opts.shift) { - if (selcount) { - if (!sel || selcount != 1) { - bounds = [ rownum, this.pivotrow ]; - this.viewport.select($A($R(bounds.min(), bounds.max())), { range: true }); - } - return; - } - } else if (opts.ctrl) { - this.pivotrow = rownum; - if (sel) { - this.viewport.deselect(row, { right: opts.right }); - return; - } else if (opts.right || selcount) { - this.viewport.select(row, { add: true, right: opts.right }); - return; - } - } - - this.viewport.select(row, { right: opts.right }); - }, - - selectAll: function() - { - this.viewport.select(this.viewport.getAllRows(), { range: true }); - }, - - isSelected: function(format, data) - { - return this.viewport.getSelected().contains(format, data); - }, - - selectedCount: function() - { - return (this.viewport) ? this.viewport.getSelected().size() : 0; - }, - - resetSelected: function() - { - if (this.viewport) { - this.viewport.deselect(this.viewport.getSelected(), { clearall: true }); - } - this.toggleButtons(); - this.clearPreviewPane(); - }, - - // num = (integer) See absolute. - // absolute = Is num an absolute row number - from 1 -> page_size (true) - - // or a relative change from the current selected value (false) - // If no current selected value, the first message in the - // current viewport is selected. - moveSelected: function(num, absolute) - { - var curr, curr_row, row, row_data, sel; - - if (absolute) { - if (!this.viewport.getMetaData('total_rows')) { - return; - } - curr = num; - } else { - if (num == 0) { - return; - } - - sel = this.viewport.getSelected(); - switch (sel.size()) { - case 0: - curr = this.viewport.currentOffset(); - curr += (num > 0) ? 1 : this.viewport.getPageSize('current'); - break; - - case 1: - curr_row = sel.get('dataob').first(); - curr = curr_row.VP_rownum + num; - break; - - default: - sel = sel.get('rownum'); - curr = (num > 0 ? sel.max() : sel.min()) + num; - break; - } - curr = (num > 0) ? Math.min(curr, this.viewport.getMetaData('total_rows')) : Math.max(curr, 1); - } - - row = this.viewport.createSelection('rownum', curr); - if (row.size()) { - row_data = row.get('dataob').first(); - if (!curr_row || row_data.imapuid != curr_row.imapuid) { - this.viewport.scrollTo(row_data.VP_rownum); - this.viewport.select(row, { delay: 0.3 }); - } - } else { - this.rownum = curr; - this.viewport.requestContentRefresh(curr - 1); - } - }, - // End message selection functions - - go: function(loc, data) - { - var app, f, separator; - - /* If switching from options, we need to reload page to pick up any - * prefs changes. */ - if (this.folder === null && - loc != 'options' && - $('appoptions') && - $('appoptions').hasClassName('on')) { - $('dimpPage').hide(); - $('dimpLoading').show(); - return DimpCore.redirect(DIMP.conf.URI_DIMP + '#' + loc, true); - } - - if (loc.startsWith('compose:')) { - return; - } - - if (loc.startsWith('msg:')) { - separator = loc.indexOf(':', 4); - f = loc.substring(4, separator); - this.uid = parseInt(loc.substring(separator + 1), 10); - loc = 'folder:' + f; - // Now fall through to the 'folder:' check below. - } - - if (loc.startsWith('folder:')) { - f = loc.substring(7); - if (this.folder != f || !$('dimpmain_folder').visible()) { - this.highlightSidebar(this.getFolderId(f)); - if (!$('dimpmain_folder').visible()) { - $('dimpmain_portal').hide(); - $('dimpmain_folder').show(); - } - - // This catches the refresh case - no need to re-add to history - if (!Object.isUndefined(this.folder) && !this.search) { - location.hash = encodeURIComponent(loc); - } - } - - this.loadMailbox(f); - return; - } - - f = this.folder; - this.folder = null; - $('dimpmain_folder').hide(); - $('dimpmain_portal').update(DIMP.text.loading).show(); - - if (loc.startsWith('app:')) { - app = loc.substr(4); - if (app == 'imp') { - this.go('folder:INBOX'); - return; - } - this.highlightSidebar('app' + app); - location.hash = encodeURIComponent(loc); - if (data) { - this.iframeContent(loc, data); - } else if (DIMP.conf.app_urls[app]) { - this.iframeContent(loc, DIMP.conf.app_urls[app]); - } - return; - } - - switch (loc) { - case 'search': - // data: 'edit_query' = folder to edit; otherwise, loads search - // screen with current mailbox as default search mailbox - if (!data) { - data = { search_mailbox: f }; - } - this.highlightSidebar(); - DimpCore.setTitle(DIMP.text.search); - this.iframeContent(loc, DimpCore.addURLParam(DIMP.conf.URI_SEARCH, data)); - break; - - case 'portal': - this.highlightSidebar('appportal'); - location.hash = encodeURIComponent(loc); - DimpCore.setTitle(DIMP.text.portal); - DimpCore.doAction('showPortal', {}, { callback: this._portalCallback.bind(this) }); - break; - - case 'options': - this.highlightSidebar('appoptions'); - location.hash = encodeURIComponent(loc); - DimpCore.setTitle(DIMP.text.prefs); - this.iframeContent(loc, DIMP.conf.URI_PREFS_IMP); - break; - } - }, - - highlightSidebar: function(id) - { - // Folder bar may not be fully loaded yet. - if ($('foldersLoading').visible()) { - this.highlightSidebar.bind(this, id).defer(); - return; - } - - var curr = $('sidebar').down('.on'), - elt = $(id); - - if (curr == elt) { - return; - } - - if (elt && !elt.match('LI')) { - elt = elt.up(); - if (!elt) { - return; - } - } - - if (curr) { - curr.removeClassName('on'); - } - - if (elt) { - elt.addClassName('on'); - this._toggleSubFolder(elt, 'exp'); - } - }, - - iframeContent: function(name, loc) - { - var container = $('dimpmain_portal'), iframe; - if (!container) { - DimpCore.showNotifications([ { type: 'horde.error', message: 'Bad portal!' } ]); - return; - } - - iframe = new Element('IFRAME', { id: 'iframe' + (name === null ? loc : name), className: 'iframe', frameBorder: 0, src: loc }).setStyle({ height: document.viewport.getHeight() + 'px' }); - container.insert(iframe); - }, - - // r = ViewPort row data - msgWindow: function(r) - { - this.updateSeenUID(r, 1); - var url = DIMP.conf.URI_MESSAGE; - url += (url.include('?') ? '&' : '?') + - $H({ folder: r.view, - uid: Number(r.imapuid) }).toQueryString(); - DimpCore.popupWindow(url, 'msgview' + r.view + r.imapuid); - }, - - composeMailbox: function(type) - { - var sel = this.viewport.getSelected(); - if (!sel.size()) { - return; - } - sel.get('dataob').each(function(s) { - DimpCore.compose(type, { folder: s.view, uid: s.imapuid }); - }); - }, - - loadMailbox: function(f, opts) - { - var need_delete; - opts = opts || {}; - - if (!this.viewport) { - this._createViewPort(); - } - - if (!opts.background) { - this.resetSelected(); - this.quicksearchClear(true); - - if (this.folder != f) { - $('folderName').update(DIMP.text.loading); - $('msgHeader').update(); - this.folderswitch = true; - - /* Don't cache results of search folders - since we will need - * to grab new copy if we ever return to it. */ - if (this.isSearch(this.folder)) { - need_delete = this.folder; - } - - this.folder = f; - - if (this.isSearch(f)) { - if (!this.search || this.search.flag) { - this._quicksearchDeactivate(!this.search); - } - $('refreshlink').show(); - } else { - $('refreshlink').hide(); - } - } - } - - this.viewport.loadView(f, { search: (this.uid ? { imapuid: Number(this.uid) } : null), background: opts.background}); - - if (need_delete) { - this.viewport.deleteView(need_delete); - } - }, - - _createViewPort: function() - { - var container = $('msgSplitPane'); - - [ $('msglistHeader') ].invoke(DIMP.conf.preview_pref == 'vert' ? 'hide' : 'show'); - - this.template = { - horiz: new Template(this.msglist_template_horiz), - vert: new Template(this.msglist_template_vert) - }; - - this.viewport = new ViewPort({ - // Mandatory config - ajax_url: DIMP.conf.URI_AJAX + 'viewPort', - container: container, - onContent: function(r, mode) { - var bg, re, u, - thread = $H(this.viewport.getMetaData('thread')), - tsort = (this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v); - - r.subjectdata = r.status = ''; - r.subjecttitle = r.subject; - - // Add thread graphics - if (tsort) { - u = thread.get(r.imapuid); - if (u) { - $R(0, u.length, true).each(function(i) { - var c = u.charAt(i); - if (!this.tcache[c]) { - this.tcache[c] = ''; - } - 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 = '' + ptr.l.truncate(10) + ''; - } - r.subjectdata += ptr.elt; - } else { - if (!ptr.elt) { - ptr.elt = '
'; - } - 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, '#{1}'); - }); - } - - // 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
    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 ul is created if necessary. - if (!ob.s && ob.ch) { - li.insert({ after: new Element('LI', { className: 'subfolders', id: submboxid }).insert(new Element('UL')).hide() }); - } - } - - li.store('ftype', ftype); - - // Make the new folder a drop target. - if (!ob.v) { - new Drop(li, this._folderDropConfig); - } - - // Check for unseen messages - if (ob.po) { - li.store('u', ''); - this.setFolderLabel(mbox, ob.u); - } - - switch (ftype) { - case 'special': - // For purposes of the contextmenu, treat special folders - // like regular folders. - ftype = 'folder'; - // Fall through. - - case 'container': - case 'folder': - new Drag(li, this._folderDragConfig); - DimpCore.addContextMenu({ - id: fid, - type: ftype - }); - break; - - case 'scontainer': - case 'virtual': - DimpCore.addContextMenu({ - id: fid, - type: (ob.v == 2) ? 'vfolder' : 'noactions' - }); - break; - } - }, - - deleteFolder: function(folder) - { - if (this.folder == folder) { - this.go('folder:INBOX'); - } - this.deleteFolderElt(this.getFolderId(folder), true); - }, - - changeFolder: function(ob) - { - var fdiv, oldexpand, - fid = this.getFolderId(ob.m); - - if ($(fid + '_special')) { - // The case of children being added to a special folder is - // handled by createFolder(). - if (!ob.ch) { - this.deleteFolderElt(fid + '_special', true); - } - return; - } - - fdiv = $(fid).down('DIV'); - oldexpand = fdiv && fdiv.hasClassName('col'); - - this.deleteFolderElt(fid, !ob.ch); - if (ob.co && this.folder == ob.m) { - this.go('folder:INBOX'); - } - this.createFolder(ob); - if (ob.ch && oldexpand) { - fdiv.removeClassName('exp').addClassName('col'); - } - }, - - deleteFolderElt: function(fid, sub) - { - var f = $(fid), submbox; - if (!f) { - return; - } - - if (sub) { - submbox = $(this.getSubFolderId(fid)); - if (submbox) { - submbox.remove(); - } - } - [ DragDrop.Drags.getDrag(fid), DragDrop.Drops.getDrop(fid) ].compact().invoke('destroy'); - this._removeMouseEvents(f); - if (this.viewport) { - this.viewport.deleteView(fid); - } - f.remove(); - }, - - _sizeFolderlist: function() - { - var nf = $('normalfolders'); - nf.setStyle({ height: (document.viewport.getHeight() - nf.cumulativeOffset()[1]) + 'px' }); - }, - - toggleSubscribed: function() - { - this.showunsub = !this.showunsub; - $('ctx_folderopts_sub', 'ctx_folderopts_unsub').invoke('toggle'); - this._reloadFolders(); - }, - - _reloadFolders: function() - { - $('foldersLoading').show(); - $('foldersSidebar').hide(); - - [ $('specialfolders').childElements(), $('dropbase').nextSiblings() ].flatten().each(function(elt) { - this.deleteFolderElt(elt.readAttribute('id'), true); - }, this); - - this._listFolders({ reload: 1, mboxes: this.folder }); - }, - - subscribeFolder: function(f, sub) - { - var fid = this.getFolderId(f); - DimpCore.doAction('subscribe', { mbox: f, sub: Number(sub) }); - - if (this.showunsub) { - [ $(fid) ].invoke(sub ? 'removeClassName' : 'addClassName', 'unsubFolder'); - } else if (!sub) { - this.deleteFolderElt(fid); - } - }, - - /* Flag actions for message list. */ - _getFlagSelection: function(opts) - { - var vs; - - if (opts.vs) { - vs = opts.vs; - } else if (opts.uid) { - vs = opts.mailbox - ? this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ opts.uid ] }, view: { equal: [ opts.mailbox ] } }) - : this.viewport.createSelection('dataob', opts.uid); - } else { - vs = this.viewport.getSelected(); - } - - return vs; - }, - - _doMsgAction: function(type, opts, args) - { - var vs = this._getFlagSelection(opts); - - if (vs.size()) { - // This needs to be synchronous Ajax if we are calling from a - // popup window because Mozilla will not correctly call the - // callback function if the calling window has been closed. - DimpCore.doAction(type, this.viewport.addRequestParams(args), { uids: vs, ajaxopts: { asynchronous: !(opts.uid && opts.mailbox) } }); - return vs; - } - - return false; - }, - - // spam = (boolean) True for spam, false for innocent - // opts = 'mailbox', 'uid' - reportSpam: function(spam, opts) - { - opts = opts || {}; - if (this._doMsgAction('reportSpam', opts, { spam: Number(spam) })) { - // Indicate to the user that something is happening (since spam - // reporting may not be instantaneous). - this.loadingImg('viewport', true); - } - }, - - // blacklist = (boolean) True for blacklist, false for whitelist - // opts = 'mailbox', 'uid' - blacklist: function(blacklist, opts) - { - opts = opts || {}; - this._doMsgAction('blacklist', opts, { blacklist: blacklist }); - }, - - // opts = 'mailbox', 'uid' - deleteMsg: function(opts) - { - opts = opts || {}; - var vs = this._getFlagSelection(opts); - - // Make sure that any given row is not deleted more than once. Need to - // explicitly mark here because message may already be flagged deleted - // when we load page (i.e. switching to using trash folder). - vs = vs.search({ isdel: { notequal: [ true ] } }); - if (!vs.size()) { - return; - } - vs.set({ isdel: true }); - - opts.vs = vs; - - this._doMsgAction('deleteMessages', opts, {}); - this.updateFlag(vs, '\\deleted', true); - }, - - // flag = (string) IMAP flag name - // set = (boolean) True to set flag - // opts = (Object) 'mailbox', 'noserver', 'uid' - flag: function(flag, set, opts) - { - opts = opts || {}; - var flags = [ (set ? '' : '-') + flag ], - vs = this._getFlagSelection(opts); - - if (!vs.size()) { - return; - } - - switch (flag) { - case '\\answered': - if (set) { - this.updateFlag(vs, '\\flagged', false); - flags.push('-\\flagged'); - } - break; - - case '\\deleted': - vs.set({ isdel: false }); - break; - - case '\\seen': - vs.get('dataob').each(function(s) { - this.updateSeenUID(s, set); - }, this); - break; - } - - this.updateFlag(vs, flag, set); - if (!opts.noserver) { - DimpCore.doAction('flagMessages', this.viewport.addRequestParams({ flags: flags.toJSON(), view: this.folder }), { uids: vs }); - } - }, - - // type = (string) 'seen' or 'unseen' - // mbox = (string) The mailbox to flag - flagAll: function(type, set, mbox) - { - DimpCore.doAction('flagAll', { flags: [ type ].toJSON(), set: Number(set), mbox: mbox }, { callback: this._flagAllCallback.bind(this) }); - }, - - hasFlag: function(f, r) - { - return this.convertFlag(f, r.flag ? r.flag.include(f) : false); - }, - - convertFlag: function(f, set) - { - /* For some flags, we need to do an inverse match (e.g. knowing a - * message is SEEN is not as important as knowing the message lacks - * the SEEN FLAG). This function will determine if, for a given flag, - * the inverse action should be taken on it. */ - return DIMP.conf.flags[f].n ? !set : set; - }, - - updateFlag: function(vs, flag, add) - { - var s = {}; - add = this.convertFlag(flag, add); - - vs.get('dataob').each(function(ob) { - this._updateFlag(ob, flag, add); - - if (this.isSearch()) { - if (s[ob.view]) { - s[ob.view].push(ob.imapuid); - } else { - s[ob.view] = [ ob.imapuid ]; - } - } - }, this); - - /* If this is a search mailbox, also need to update flag in base view, - * if it is in the buffer. */ - $H(s).each(function(m) { - var tmp = this.viewport.getSelection(m.key).search({ imapuid: { equal: m.value }, view: { equal: m.key } }); - if (tmp.size()) { - this._updateFlag(tmp.get('dataob').first(), flag, add); - } - }, this); - }, - - _updateFlag: function(ob, flag, add) - { - ob.flag = ob.flag - ? ob.flag.without(flag) - : []; - - if (add) { - ob.flag.push(flag); - } - - this.viewport.updateRow(ob); - }, - - /* Miscellaneous folder actions. */ - purgeDeleted: function() - { - DimpCore.doAction('purgeDeleted', this.viewport.addRequestParams({})); - }, - - modifyPoll: function(folder, add) - { - DimpCore.doAction('modifyPoll', { add: Number(add), mbox: folder }, { callback: this._modifyPollCallback.bind(this) }); - }, - - _modifyPollCallback: function(r) - { - r = r.response; - var f = r.mbox, fid, p = { response: { poll: {} } }; - fid = $(this.getFolderId(f)); - - if (r.add) { - p.response.poll[f] = r.poll.u; - fid.store('u', 0); - } else { - p.response.poll[f] = 0; - } - - if (!r.add) { - fid.store('u', null); - this.updateUnseenStatus(f, 0); - } - }, - - loadingImg: function(id, show) - { - DimpCore.loadingImg(id + 'Loading', id == 'viewport' ? 'msgSplitPane' : 'previewPane', show); - }, - - // p = (element) Parent element - // c = (element) Child element - isSubfolder: function(p, c) - { - var sf = $(this.getSubFolderId(p.identify())); - return sf && c.descendantOf(sf); - }, - - /* Pref updating function. */ - _updatePrefs: function(pref, value) - { - new Ajax.Request(DimpCore.addURLParam(DIMP.conf.URI_PREFS), { parameters: { pref: pref, value: value } }); - }, - - /* Onload function. */ - onDomLoad: function() - { - DimpCore.init(); - - var DM = DimpCore.DMenu, tmp; - - /* Register global handlers now. */ - document.observe('keydown', this.keydownHandler.bindAsEventListener(this)); - document.observe('change', this.changeHandler.bindAsEventListener(this)); - document.observe('dblclick', this.dblclickHandler.bindAsEventListener(this)); - Event.observe(window, 'resize', this.onResize.bind(this)); - - /* Limit to folders sidebar only. */ - $('foldersSidebar').observe('mouseover', this.mouseoverHandler.bindAsEventListener(this)); - - /* Show page now. */ - $('sidebar').setStyle({ width: DIMP.conf.sidebar_width }); - $('dimpLoading').hide(); - $('dimpPage').show(); - - /* Create splitbar for sidebar. */ - this.splitbar = new Element('DIV', { className: 'splitBarVertSidebar' }).setStyle({ height: document.viewport.getHeight() + 'px', left: $('sidebar').clientWidth + 'px' }); - $('sidebar').insert({ after: this.splitbar }); - new Drag(this.splitbar, { - constraint: 'horizontal', - ghosting: true, - nodrop: true - }); - - $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + this.splitbar.clientWidth) + 'px' }); - - /* Init quicksearch. These needs to occur before loading the message - * list since it may be disabled if we are in a search mailbox. */ - if ($('qsearch')) { - $('qsearch_input').observe('blur', this._quicksearchOnBlur.bind(this)); - DimpCore.addContextMenu({ - id: 'qsearch_icon', - left: true, - offset: 'qsearch', - type: 'qsearchopts' - }); - DimpCore.addContextMenu({ - id: 'qsearch_icon', - left: false, - offset: 'qsearch', - type: 'qsearchopts' - }); - DM.addSubMenu('ctx_qsearchopts_by', 'ctx_qsearchby'); - DM.addSubMenu('ctx_qsearchopts_filter', 'ctx_flag'); - DM.addSubMenu('ctx_qsearchopts_filternot', 'ctx_flag'); - } - - /* Store these text strings for updating purposes. */ - DIMP.text.getmail = $('checkmaillink').down('A').innerHTML; - DIMP.text.refresh = $('refreshlink').down('A').innerHTML; - DIMP.text.showalog = $('alertsloglink').down('A').innerHTML; - - /* Initialize the starting page. */ - tmp = location.hash; - if (!tmp.empty() && tmp.startsWith('#')) { - tmp = (tmp.length == 1) ? "" : tmp.substring(1); - } - - if (!tmp.empty()) { - this.go(decodeURIComponent(tmp)); - } else if (DIMP.conf.login_view == 'inbox') { - this.go('folder:INBOX'); - } else { - this.go('portal'); - this.loadMailbox('INBOX', { background: true }); - } - - /* Create the folder list. Any pending notifications will be caught - * via the return from this call. */ - this._listFolders({ initial: 1, mboxes: this.folder} ); - - this._setQsearchText(true); - - /* Add popdown menus. Check for disabled compose at the same time. */ - DimpCore.addPopdown('button_other', 'otheractions', true); - DimpCore.addPopdown('folderopts_link', 'folderopts', true); - - DM.addSubMenu('ctx_message_reply', 'ctx_reply'); - DM.addSubMenu('ctx_message_forward', 'ctx_forward'); - [ 'ctx_message_', 'oa_' ].each(function(i) { - if ($(i + 'setflag')) { - DM.addSubMenu(i + 'setflag', 'ctx_flag'); - DM.addSubMenu(i + 'unsetflag', 'ctx_flag'); - } - }); - DM.addSubMenu('ctx_folder_setflag', 'ctx_folder_flag'); - - if (DIMP.conf.disable_compose) { - $('button_reply', 'button_forward').compact().invoke('up', 'SPAN').concat($('button_compose', 'composelink', 'ctx_contacts_new')).compact().invoke('remove'); - } else { - DimpCore.addPopdown('button_reply', 'reply', false, true); - DimpCore.addPopdown('button_forward', 'forward', false, true); - } - - DimpCore.addContextMenu({ - id: 'msglistHeader', - type: 'mboxsort' - }); - - new Drop('dropbase', this._folderDropConfig); - - if (DIMP.conf.toggle_pref) { - this._toggleHeaders($('th_expand')); - } - - /* Remove unavailable menu items. */ - if (!$('GrowlerLog')) { - $('alertsloglink').remove(); - } - - /* Check for new mail. */ - this.setPoll(); - }, - - /* Resize function. */ - onResize: function() - { - if (this.resize) { - clearTimeout(this.resize); - } - - this.resize = this._onResize.bind(this).delay(0.1); - }, - - _onResize: function() - { - this._sizeFolderlist(); - this.splitbar.setStyle({ height: document.viewport.getHeight() + 'px' }); - }, - - /* Extend AJAX exception handling. */ - onAjaxException: function(parentfunc, r, e) - { - /* Make sure loading images are closed. */ - this.loadingImg('msg', false); - this.loadingImg('viewport', false); - DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_error } ]); - parentfunc(r, e); - } - -}; - -/* Need to add after DimpBase is defined. */ -DimpBase._msgDragConfig = { - classname: 'msgdrag', - scroll: 'normalfolders', - threshold: 5, - caption: DimpBase.dragCaption.bind(DimpBase) -}; - -DimpBase._folderDragConfig = { - classname: 'folderdrag', - ghosting: true, - offset: { x: 15, y: 0 }, - scroll: 'normalfolders', - threshold: 5 -}; - -DimpBase._folderDropConfig = { - caption: function(drop, drag, e) { - var m, - d = drag.retrieve('l'), - ftype = drop.retrieve('ftype'), - l = drop.retrieve('l'); - - if (drop == $('dropbase')) { - return DIMP.text.moveto.sub('%s', d).sub('%s', DIMP.text.baselevel); - } - - switch (e.type) { - case 'mousemove': - m = (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto; - break; - - case 'keydown': - /* Can't use ctrlKey here since different browsers handle the - * ctrlKey in different ways when it comes to firing keyboard - * events. */ - m = (e.keyCode == 17) ? DIMP.text.copyto : DIMP.text.moveto; - break; - - case 'keyup': - m = (e.keyCode == 17) - ? DIMP.text.moveto - : (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto; - break; - } - - if (drag.hasClassName('folder')) { - return (ftype != 'special' && !DimpBase.isSubfolder(drag, drop)) ? m.sub('%s', d).sub('%s', l) : ''; - } - - return ftype != 'container' ? m.sub('%s', DimpBase.dragCaption()).sub('%s', l) : ''; - }, - keypress: true -}; - -/* Drag/drop listeners. */ -document.observe('DragDrop2:drag', DimpBase.onDrag.bindAsEventListener(DimpBase)); -document.observe('DragDrop2:drop', DimpBase.folderDropHandler.bindAsEventListener(DimpBase)); -document.observe('DragDrop2:end', DimpBase.onDragEnd.bindAsEventListener(DimpBase)); -document.observe('DragDrop2:mousedown', DimpBase.onDragMouseDown.bindAsEventListener(DimpBase)); -document.observe('DragDrop2:mouseup', DimpBase.onDragMouseUp.bindAsEventListener(DimpBase)); - -/* Route AJAX responses through ViewPort. */ -DimpCore.onDoActionComplete = function(r) { - DimpBase.deleteCallback(r); - if (DimpBase.viewport) { - DimpBase.viewport.parseJSONResponse(r); - } - DimpBase.pollCallback(r); -}; - -/* Click handler. */ -DimpCore.clickHandler = DimpCore.clickHandler.wrap(DimpBase.clickHandler.bind(DimpBase)); - -/* ContextSensitive handlers. */ -DimpCore.contextOnClick = DimpCore.contextOnClick.wrap(DimpBase.contextOnClick.bind(DimpBase)); -DimpCore.contextOnShow = DimpCore.contextOnShow.wrap(DimpBase.contextOnShow.bind(DimpBase)); - -/* Extend AJAX exception handling. */ -DimpCore.doActionOpts.onException = DimpCore.doActionOpts.onException.wrap(DimpBase.onAjaxException.bind(DimpBase)); - -/* Initialize onload handler. */ -document.observe('dom:loaded', DimpBase.onDomLoad.bind(DimpBase)); diff --git a/imp/js/DimpCore.js b/imp/js/DimpCore.js deleted file mode 100644 index 04cbc29e6..000000000 --- a/imp/js/DimpCore.js +++ /dev/null @@ -1,595 +0,0 @@ -/** - * DimpCore.js - Dimp UI application logic. - * - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * See the enclosed file COPYING for license information (GPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. - */ - -/* DimpCore object. */ -var DimpCore = { - // Vars used and defaulting to null/false: - // DMenu, Growler, inAjaxCallback, is_init, is_logout - // onDoActionComplete - alarms: {}, - growler_log: true, - server_error: 0, - - doActionOpts: { - onException: function(r, e) { DimpCore.debug('onException', e); }, - onFailure: function(t, o) { DimpCore.debug('onFailure', t); }, - evalJS: false, - evalJSON: true - }, - - debug: function(label, e) - { - if (!this.is_logout && window.console && window.console.error) { - window.console.error(label, Prototype.Browser.Gecko ? e : $H(e).inspect()); - } - }, - - // Convert object to an IMP UID Range string. See IMP::toRangeString() - // ob = (object) mailbox name as keys, values are array of uids. - toRangeString: function(ob) - { - var str = ''; - - $H(ob).each(function(o) { - if (!o.value.size()) { - return; - } - - var u = o.value.numericSort(), - first = u.shift(), - last = first, - out = []; - - u.each(function(k) { - if (last + 1 == k) { - last = k; - } else { - out.push(first + (last == first ? '' : (':' + last))); - first = last = k; - } - }); - out.push(first + (last == first ? '' : (':' + last))); - str += '{' + o.key.length + '}' + o.key + out.join(','); - }); - - return str; - }, - - // Parses an IMP UID Range string. See IMP::parseRangeString() - // str = (string) An IMP UID range string. - parseRangeString: function(str) - { - var count, end, i, mbox, uidstr, - mlist = {}, - uids = []; - str = str.strip(); - - while (!str.blank()) { - if (!str.startsWith('{')) { - break; - } - i = str.indexOf('}'); - count = Number(str.substr(1, i - 1)); - mbox = str.substr(i + 1, count); - i += count + 1; - end = str.indexOf('{', i); - if (end == -1) { - uidstr = str.substr(i); - str = ''; - } else { - uidstr = str.substr(i, end - i); - str = str.substr(end); - } - - uidstr.split(',').each(function(e) { - var r = e.split(':'); - if (r.size() == 1) { - uids.push(Number(e)); - } else { - uids = uids.concat($A($R(Number(r[0]), Number(r[1])))); - } - }); - - mlist[mbox] = uids; - } - - return mlist; - }, - - // 'opts' -> ajaxopts, callback, uids - doAction: function(action, params, opts) - { - params = $H(params); - opts = opts || {}; - - var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {}); - - if (opts.uids) { - if (opts.uids.viewport_selection) { - opts.uids = this.selectionToRange(opts.uids); - } - params.set('uid', this.toRangeString(opts.uids)); - } - - ajaxopts.parameters = this.addRequestParams(params); - ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this); - - new Ajax.Request(DIMP.conf.URI_AJAX + action, ajaxopts); - }, - - // 'opts' -> ajaxopts, callback - submitForm: function(form, opts) - { - opts = opts || {}; - var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {}); - ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this); - $(form).request(ajaxopts); - }, - - selectionToRange: function(s) - { - var b = s.getBuffer(), - tmp = {}; - - if (b.getMetaData('search')) { - s.get('uid').each(function(r) { - var parts = r.split(DIMP.conf.IDX_SEP); - if (tmp[parts[0]]) { - tmp[parts[0]].push(parts[1]); - } else { - tmp[parts[0]] = [ parts[1] ]; - } - }); - } else { - tmp[b.getView()] = s.get('uid'); - } - - return tmp; - }, - - // params - (Hash) - addRequestParams: function(params) - { - var p = params.clone(); - - if (DIMP.conf.SESSION_ID) { - p.update(DIMP.conf.SESSION_ID.toQueryParams()); - } - - return p; - }, - - doActionComplete: function(request, callback) - { - this.inAjaxCallback = true; - - if (!request.responseJSON) { - if (++this.server_error == 3) { - this.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_timeout } ]); - } - this.inAjaxCallback = false; - return; - } - - var r = request.responseJSON; - - if (!r.msgs) { - r.msgs = []; - } - - if (r.response && Object.isFunction(callback)) { - try { - callback(r); - } catch (e) { - this.debug('doActionComplete', e); - } - } - - if (this.server_error >= 3) { - r.msgs.push({ type: 'horde.success', message: DIMP.text.ajax_recover }); - } - this.server_error = 0; - - this.showNotifications(r.msgs); - - if (r.response && this.onDoActionComplete) { - this.onDoActionComplete(r.response); - } - - this.inAjaxCallback = false; - }, - - setTitle: function(title) - { - document.title = DIMP.conf.name + ' :: ' + title; - }, - - showNotifications: function(msgs) - { - if (!msgs.size() || this.is_logout) { - return; - } - - msgs.find(function(m) { - switch (m.type) { - case 'horde.ajaxtimeout': - this.logout(m.message); - return true; - - case 'horde.alarm': - if (!this.alarms[m.flags.alarm.id]) { - this.Growler.growl(m.flags.alarm.title + ': ' + m.flags.alarm.text, { - className: 'horde-alarm', - sticky: 1, - log: 1 - }); - this.alarms[m.flags.alarm.id] = 1; - } - break; - - case 'horde.error': - case 'horde.message': - case 'horde.success': - case 'horde.warning': - this.Growler.growl(m.message, { - className: m.type.replace('.', '-'), - life: (m.type == 'horde.error' ? 12 : 8), - log: 1 - }); - break; - - case 'imp.reply': - case 'imp.forward': - case 'imp.redirect': - this.Growler.growl(m.message, { - className: m.type.replace('.', '-'), - life: 8 - }); - break; - } - }, this); - }, - - compose: function(type, args) - { - var url = DIMP.conf.URI_COMPOSE; - args = args || {}; - if (type) { - args.type = type; - } - this.popupWindow(this.addURLParam(url, args), 'compose' + new Date().getTime()); - }, - - popupWindow: function(url, name, onload) - { - var opts = { - height: DIMP.conf.popup_height, - name: name.gsub(/\W/, '_'), - noalert: true, - onload: onload, - url: url, - width: DIMP.conf.popup_width - }; - - if (!Horde.popup(opts)) { - this.showNotifications([ { type: 'horde.warning', message: DIMP.text.popup_block } ]); - } - }, - - closePopup: function() - { - // Mozilla bug/feature: it will not close a browser window - // automatically if there is code remaining to be performed (or, at - // least, not here) unless the mouse is moved or a keyboard event - // is triggered after the callback is complete. (As of FF 2.0.0.3 and - // 1.5.0.11). So wait for the callback to complete before attempting - // to close the window. - if (this.inAjaxCallback) { - this.closePopup.bind(this).defer(); - } else { - window.close(); - } - }, - - logout: function(url) - { - this.is_logout = true; - this.redirect(url || (DIMP.conf.URI_AJAX + 'logOut')); - }, - - redirect: function(url, force) - { - var ptr = parent.frames.horde_main ? parent : window; - - ptr.location.assign(this.addURLParam(url)); - - // Catch browsers that don't redirect on assign(). - if (force && !Prototype.Browser.WebKit) { - (function() { ptr.location.reload(); }).delay(0.5); - } - }, - - loadingImg: function(elt, id, show) - { - elt = $(elt); - - if (show) { - elt.clonePosition(id, { setHeight: false, setLeft: false, setWidth: false }).show(); - } else { - elt.fade({ duration: 0.2 }); - } - }, - - toggleButtons: function(elts, disable) - { - elts.each(function(b) { - var tmp; - [ b.up() ].invoke(disable ? 'addClassName' : 'removeClassName', 'disabled'); - if (this.DMenu && - (tmp = b.next('.popdown'))) { - this.DMenu.disable(tmp.identify(), true, disable); - } - }, this); - }, - - // p = (Element) Parent element - // t = (string) Context menu type - // trigger = (boolean) Trigger popdown on button click? - // d = (boolean) Disabled? - addPopdown: function(p, t, trigger, d) - { - var elt = new Element('SPAN', { className: 'iconImg popdownImg popdown' }); - p = $(p); - - p.insert({ after: elt }); - - if (trigger) { - this.addContextMenu({ - disable: d, - id: p.identify(), - left: true, - offset: p.up(), - type: t - }); - } - - this.addContextMenu({ - disable: d, - id: elt.identify(), - left: true, - offset: elt.up(), - type: t - }); - - return elt; - }, - - addContextMenu: function(p) - { - if (this.DMenu) { - this.DMenu.addElement(p.id, 'ctx_' + p.type, p); - } - }, - - /* Add dropdown menus to addresses. */ - buildAddressLinks: function(alist, elt) - { - var base, tmp, - cnt = alist.size(); - - if (cnt > 15) { - tmp = $('largeaddrspan').cloneNode(true).writeAttribute('id', 'largeaddrspan_active'); - elt.insert(tmp); - base = tmp.down('.dispaddrlist'); - tmp = tmp.down('.largeaddrlist'); - tmp.setText(tmp.getText().replace('%d', cnt)); - } else { - base = elt; - } - - alist.each(function(o, i) { - var a; - if (o.raw) { - a = o.raw; - } else { - a = new Element('A', { className: 'address' }).store({ personal: o.personal, email: o.inner, address: (o.personal ? (o.personal + ' <' + o.inner + '>') : o.inner) }); - if (o.personal) { - a.writeAttribute({ title: o.inner }).insert(o.personal.escapeHTML()); - } else { - a.insert(o.inner.escapeHTML()); - } - this.DMenu.addElement(a.identify(), 'ctx_contacts', { offset: a, left: true }); - } - base.insert(a); - if (i + 1 != cnt) { - base.insert(', '); - } - }, this); - - return elt; - }, - - /* Add message log info to message view. */ - updateMsgLog: function(log) - { - var tmp = ''; - log.each(function(entry) { - tmp += '
  • ' + entry.m + '
  • '; - }); - $('msgloglist').down('UL').update(tmp); - }, - - /* Removes event handlers from address links. */ - removeAddressLinks: function(id) - { - id.select('.address').each(function(elt) { - this.DMenu.removeElement(elt.identify()); - }, this); - }, - - addURLParam: function(url, params) - { - var q = url.indexOf('?'); - params = $H(params); - - if (DIMP.conf.SESSION_ID) { - params.update(DIMP.conf.SESSION_ID.toQueryParams()); - } - - if (q != -1) { - params.update(url.toQueryParams()); - url = url.substring(0, q); - } - - return params.size() ? (url + '?' + params.toQueryString()) : url; - }, - - reloadMessage: function(params) - { - if (typeof DimpFullmessage != 'undefined') { - window.location = this.addURLParam(document.location.href, params); - } else { - DimpBase.loadPreview(null, params); - } - }, - - /* Mouse click handler. */ - clickHandler: function(e) - { - if (e.isRightClick()) { - return; - } - - var elt = e.element(), id, tmp; - - while (Object.isElement(elt)) { - id = elt.readAttribute('id'); - - switch (id) { - case 'largeaddrspan_active': - tmp = elt.down(); - if (!tmp.next().visible() || - e.element().hasClassName('largeaddrlist')) { - [ tmp.down(), tmp.down(1), tmp.next() ].invoke('toggle'); - } - break; - - default: - // CSS class based matching - if (elt.hasClassName('unblockImageLink')) { - IMP.unblockImages(e); - } else if (elt.hasClassName('toggleQuoteShow')) { - [ elt, elt.next() ].invoke('toggle'); - elt.next(1).blindDown({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } }); - } else if (elt.hasClassName('toggleQuoteHide')) { - [ elt, elt.previous() ].invoke('toggle'); - elt.next().blindUp({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } }); - } else if (elt.hasClassName('pgpVerifyMsg')) { - elt.replace(DIMP.text.verify); - DimpCore.reloadMessage({ pgp_verify_msg: 1 }); - e.stop(); - } else if (elt.hasClassName('smimeVerifyMsg')) { - elt.replace(DIMP.text.verify); - DimpCore.reloadMessage({ smime_verify_msg: 1 }); - e.stop(); - } - break; - } - - elt = elt.up(); - } - }, - - contextOnShow: function(e) - { - var tmp; - - switch (e.memo) { - case 'ctx_contacts': - tmp = $(e.memo).down('DIV.contactAddr'); - if (tmp) { - tmp.next().remove(); - tmp.remove(); - } - - // Add e-mail info to context menu if personal name is shown on - // page. - if (e.element().retrieve('personal')) { - $(e.memo) - .insert({ top: new Element('DIV', { className: 'sep' }) }) - .insert({ top: new Element('DIV', { className: 'contactAddr' }).insert(e.element().retrieve('email').escapeHTML()) }); - } - break; - } - }, - - contextOnClick: function(e) - { - var baseelt = e.element(); - - switch (e.memo.elt.readAttribute('id')) { - case 'ctx_contacts_new': - this.compose('new', { to: baseelt.retrieve('address') }); - break; - - case 'ctx_contacts_add': - this.doAction('addContact', { name: baseelt.retrieve('personal'), email: baseelt.retrieve('email') }, {}, true); - break; - } - }, - - /* DIMP initialization function. */ - init: function() - { - if (this.is_init) { - return; - } - this.is_init = true; - - if (typeof ContextSensitive != 'undefined') { - this.DMenu = new ContextSensitive(); - document.observe('ContextSensitive:click', this.contextOnClick.bindAsEventListener(this)); - document.observe('ContextSensitive:show', this.contextOnShow.bindAsEventListener(this)); - } - - /* Add Growler notification handler. */ - this.Growler = new Growler({ - location: 'br', - log: this.growler_log, - noalerts: DIMP.text.noalerts - }); - - /* Add click handler. */ - document.observe('click', DimpCore.clickHandler.bindAsEventListener(DimpCore)); - - /* Catch dialog actions. */ - document.observe('IMPDialog:success', function(e) { - switch (e.memo) { - case 'pgpPersonal': - case 'pgpSymmetric': - case 'smimePersonal': - IMPDialog.noreload = true; - this.reloadMessage({}); - break; - } - }.bindAsEventListener(this)); - - /* Determine base window. Need a try/catch block here since, if the - * page was loaded by an opener out of this current domain, this will - * throw an exception. */ - try { - if (parent.opener && - parent.opener.location.host == window.location.host && - parent.opener.DimpCore) { - DIMP.baseWindow = parent.opener.DIMP.baseWindow || parent.opener; - } - } catch (e) {} - } - -}; diff --git a/imp/js/ViewPort.js b/imp/js/ViewPort.js deleted file mode 100644 index 22c43b99d..000000000 --- a/imp/js/ViewPort.js +++ /dev/null @@ -1,1846 +0,0 @@ -/** - * ViewPort.js - Code to create a viewport window, with optional split pane - * functionality. - * - * Usage: - * ====== - * var viewport = new ViewPort({ options }); - * - * Required options: - * ----------------- - * ajax_url: (string) The URL to send the viewport requests to. - * This URL should return its response in an object named - * 'ViewPort' (other information can be returned in the response and - * will be ignored by this class). - * container: (Element/string) A DOM element/ID of the container that holds - * the viewport. This element should be empty and have no children. - * onContent: (function) A function that takes 2 arguments - the data object - * for the row and a string indicating the current pane_mode. - * - * This function MUST return the HTML representation of the row. - * - * This representation MUST include both the DOM ID (stored in - * the VP_domid data entry) and the CSS class name (stored as an - * array in the VP_bg data entry) in the outermost element. - * - * Selected rows will contain the classname 'vpRowSelected'. - * - * - * Optional options: - * ----------------- - * ajax_opts: (object) Any additional options to pass to the Ajax.Request - * object when sending an AJAX message. - * buffer_pages: (integer) The number of viewable pages to send to the browser - * per server access when listing rows. - * empty_msg: (string) A string to display when the view is empty. Inserted in - * a SPAN element with class 'vpEmpty'. - * limit_factor: (integer) When browsing through a list, if a user comes - * within this percentage of the end of the current cached - * viewport, send a background request to the server to retrieve - * the next slice. - * list_class: (string) The CSS class to use for the list container. - * lookbehind: (integer) What percentage of the received buffer should be - * used to download rows before the given row number? - * onAjaxFailure: (function) Callback function that handles a failure response - * from an AJAX request. - * params: (XMLHttpRequest object) - * (mixed) Result of evaluating the X-JSON response - * header, if any (can be null). - * return: NONE - * onAjaxRequest: (function) Callback function that allows additional - * parameters to be added to the outgoing AJAX request. - params: (string) The current view. - return: (Hash) Parameters to add to the outgoing request. - * onAjaxResponse: (function) Callback function that allows user-defined code - * to additionally process the AJAX return data. - * params: (XMLHttpRequest object) - * (mixed) Result of evaluating the X-JSON response - * header, if any (can be null). - * return: NONE - * onCachedList: (function) Callback function that allows the cache ID string - * to be dynamically generated. - params: (string) The current view. - return: (string) The cache ID string to use. - * onContentOffset: (function) Callback function that alters the starting - * offset of the content about to be rendered. - * params: (integer) The current offset. - * return: (integer) The altered offset. - * onSlide: (function) Callback function that is triggered when the - * viewport slider bar is moved. - * params: NONE - * return: NONE - * page_size: (integer) Default page size to view on load. Only used if - * pane_mode is 'horiz'. - * pane_data: (Element/string) A DOM element/ID of the container to hold - * the split pane data. This element will be moved inside of the - * container element. - * pane_mode: (string) The split pane mode to show on load? Either empty, - * 'horiz', or 'vert'. - * pane_width: (integer) The default pane width to use on load. Only used if - * pane_mode is 'vert'. - * split_bar_class: (object) The CSS class(es) to use for the split bar. - * Takes two properties: 'horiz' and 'vert'. - * wait: (integer) How long, in seconds, to wait before displaying an - * informational message to users that the list is still being - * built. - * - * - * Custom events: - * -------------- - * Custom events are triggered on the container element. The parameters given - * below are available through the 'memo' property of the Event object. - * - * ViewPort:add - * Fired when a row has been added to the screen. - * params: (Element) The viewport row being added. - * - * ViewPort:cacheUpdate - * Fired when the internal cached data of a view is changed. - * params: (string) View which is being updated. - * - * ViewPort:clear - * Fired when a row is being removed from the screen. - * params: (Element) The viewport row being removed. - * - * ViewPort:contentComplete - * Fired when the view has changed and all viewport rows have been added. - * params: NONE - * - * ViewPort:deselect - * Fired when rows are deselected. - * params: (object) opts = (object) Boolean options [right] - * vs = (ViewPort_Selection) A ViewPort_Selection object. - * - * ViewPort:endFetch - * Fired when a fetch AJAX response is completed. - * params: (string) Current view. - * - * ViewPort:fetch - * Fired when a non-background AJAX response is sent. - * params: (string) Current view. - * - * ViewPort:select - * Fired when rows are selected. - * params: (object) opts = (object) Boolean options [delay, right] - * vs = (ViewPort_Selection) A ViewPort_Selection object. - * - * ViewPort:splitBarChange - * Fired when the splitbar is moved. - * params: (string) The current pane mode ('horiz' or 'vert'). - * - * ViewPort:splitBarEnd - * Fired when the splitbar is released. - * params: (string) The current pane mode ('horiz' or 'vert'). - * - * ViewPort:splitBarStart - * Fired when the splitbar is initially clicked. - * params: (string) The current pane mode ('horiz' or 'vert'). - * - * ViewPort:wait - * Fired if viewport_wait seconds have passed since request was sent. - * params: (string) Current view. - * - * - * Outgoing AJAX request has the following params: - * ----------------------------------------------- - * For ALL requests: - * cache: (string) The list of uids cached on the browser. - * cacheid: (string) A unique string that changes whenever the viewport - * list changes. - * initial: (integer) This is the initial browser request for this view. - * requestid: (integer) A unique identifier for this AJAX request. - * view: (string) The view of the request. - * - * For a row request: - * slice: (string) The list of rows to retrieve from the server. - * In the format: [first_row]:[last_row] - * - * For a search request: - * after: (integer) The number of rows to return after the selected row. - * before: (integer) The number of rows to return before the selected row. - * search: (JSON object) The search query. - * - * For a rangeslice request: - * rangeslice: (integer) If present, indicates that slice is a rangeslice - * request. - * slice: (string) The list of rows to retrieve from the server. - * In the format: [first_row]:[last_row] - * - * - * Incoming AJAX response has the following params: - * ------------------------------------------------ - * cacheid: (string) A unique string that changes whenever the viewport - * list changes. - * data: (object) Data for each entry that is passed to the template to create - * the viewable rows. Keys are a unique ID (see also the 'rowlist' - * entry). Values are the data objects. Internal keys for these data - * objects must NOT begin with the string 'VP_'. - * disappear: (array) If update is set, this is the list of unique IDs that - * have been cached by the browser but no longer appear on the - * server. - * label: (string) [REQUIRED when initial is true] The label to use for the - * view. - * metadata [optional]: (object) Metadata for the view. Entries in buffer are - * updated with these entries (unless resetmd is set). - * rangelist [optional]: (object) The list of unique IDs -> rownumbers that - * correspond the the given request. Only returned for - * a rangeslice request. - * requestid: (string) The request ID sent in the outgoing AJAX request. - * reset [optional]: (integer) If set, purges all cached data. - * resetmd [optional]: (integer) If set, purges all user metadata. - * rowlist: (object) A mapping of unique IDs (keys) to the row numbers - * (values). Row numbers start at 1. - * rownum [optional]: (integer) The row number to position screen on. - * totalrows: (integer) Total number of rows in the view. - * update [optional]: (integer) If set, update the rowlist instead of - * overwriting it. - * updatecacheid [optional]: (string) If set, simply update the cacheid with - * the new value. Indicates that the browser - * contains the up-to-date version of the cache. - * view: (string) The view ID of the request. - * - * - * Data entries: - * ------------- - * In addition to the data provided from the server, the following - * dynamically created entries are also available: - * VP_domid: (string) The DOM ID of the row. - * VP_id: (string) The unique ID used to store the data entry. - * VP_rownum: (integer) The row number of the row. - * - * - * Scroll bars use ars styled using these CSS class names: - * ------------------------------------------------------- - * vpScroll - The scroll bar container. - * vpScrollUp - The UP arrow. - * vpScrollCursor - The cursor used to slide within the bounds. - * vpScrollDown - The DOWN arrow. - * - * - * Requires prototypejs 1.6+, scriptaculous 1.8+ (effects.js only), and - * Horde's dragdrop2.js and slider2.js. - * - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * See the enclosed file COPYING for license information (GPL). If you - * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. - */ - -/** - * ViewPort - */ -var ViewPort = Class.create({ - - initialize: function(opts) - { - this.opts = Object.extend({ - buffer_pages: 10, - limit_factor: 35, - lookbehind: 40, - split_bar_class: {} - }, opts); - - this.opts.container = $(opts.container); - this.opts.pane_data = $(opts.pane_data); - - this.opts.content = new Element('DIV', { className: opts.list_class }).setStyle({ float: 'left', overflow: 'hidden' }); - this.opts.container.insert(this.opts.content); - - this.scroller = new ViewPort_Scroller(this); - - this.split_pane = { - curr: null, - currbar: null, - horiz: { - loc: opts.page_size - }, - init: false, - spacer: null, - vert: { - width: opts.pane_width - } - }; - this.views = {}; - - this.pane_mode = opts.pane_mode; - - this.isbusy = this.page_size = null; - this.request_num = 1; - - // Init empty string now. - this.empty_msg = new Element('SPAN', { className: 'vpEmpty' }).insert(opts.empty_msg); - - // Set up AJAX response function. - this.ajax_response = this.opts.onAjaxResponse || this._ajaxRequestComplete.bind(this); - - Event.observe(window, 'resize', this.onResize.bind(this)); - }, - - // view = (string) ID of view. - // opts = (object) background: (boolean) Load view in background? - // search: (object) Search parameters - loadView: function(view, opts) - { - var buffer, curr, ps, - f_opts = {}, - init = true; - - this._clearWait(); - - // Need a page size before we can continue - this is what determines - // the slice size to request from the server. - if (this.page_size === null) { - ps = this.getPageSize(this.pane_mode ? 'default' : 'max'); - if (isNaN(ps)) { - return this.loadView.bind(this, view, opts).defer(); - } - this.page_size = ps; - } - - if (this.view) { - if (!opts.background && (view != this.view)) { - // Need to store current buffer to save current offset - buffer = this._getBuffer(); - buffer.setMetaData({ offset: this.currentOffset() }, true); - this.views[this.view] = buffer; - } - init = false; - } - - if (opts.background) { - f_opts = { background: true, view: view }; - } else { - if (!this.view) { - this.onResize(true); - } else if (this.view != view) { - this.active_req = null; - } - this.view = view; - } - - if (curr = this.views[view]) { - this._updateContent(curr.getMetaData('offset') || 0, f_opts); - if (!opts.background) { - this._ajaxRequest({ checkcache: 1 }); - } - return; - } - - if (!init) { - this.visibleRows().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); - this.opts.content.update(); - this.scroller.clear(); - } - - this.views[view] = buffer = this._getBuffer(view, true); - - if (opts.search) { - f_opts.search = opts.search; - } else { - f_opts.offset = 0; - } - - f_opts.initial = 1; - - this._fetchBuffer(f_opts); - }, - - // view = ID of view - deleteView: function(view) - { - delete this.views[view]; - }, - - // rownum = (integer) Row number - // opts = (Object) [noupdate, top] TODO - scrollTo: function(rownum, opts) - { - var s = this.scroller; - opts = opts || {}; - - s.noupdate = opts.noupdate; - - switch (this.isVisible(rownum)) { - case -1: - s.moveScroll(rownum - 1); - break; - - case 0: - if (opts.top) { - s.moveScroll(rownum - 1); - } - break; - - case 1: - s.moveScroll(Math.min(rownum - 1, this.getMetaData('total_rows') - this.getPageSize())); - break; - } - - s.noupdate = false; - }, - - // rownum = (integer) Row number - isVisible: function(rownum) - { - var offset = this.currentOffset(); - return (rownum < offset + 1) - ? -1 - : ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0); - }, - - // params = (object) Parameters to add to outgoing URL - reload: function(params) - { - this._fetchBuffer({ - offset: this.currentOffset(), - params: $H(params), - purge: true - }); - }, - - // vs = (Viewport_Selection) A Viewport_Selection object. - // opts = (object) TODO [noupdate, view] - remove: function(vs, opts) - { - if (!vs.size()) { - return; - } - - if (this.isbusy) { - this.remove.bind(this, vs, opts).defer(); - return; - } - - this.isbusy = true; - opts = opts || {}; - - var args = { duration: 0.2, to: 0.01 }, - visible = vs.get('div'); - - this.deselect(vs); - - // If we have visible elements to remove, only call refresh after - // the last effect has finished. - if (visible.size()) { - // Set 'to' to a value slightly above 0 to prevent fade() - // from auto hiding. Hiding is unnecessary, since we will be - // removing from the document shortly. - visible.slice(0, -1).invoke('fade', args); - args.afterFinish = this._removeids.bind(this, vs, opts); - visible.last().fade(args); - } else { - this._removeids(vs, opts); - } - }, - - // vs = (Viewport_Selection) A Viewport_Selection object. - // opts = (object) TODO [noupdate, view] - _removeids: function(vs, opts) - { - this._getBuffer(opts.view).setMetaData({ total_rows: this.getMetaData('total_rows', opts.view) - vs.size() }, true); - - this._getBuffer().remove(vs.get('rownum')); - this.opts.container.fire('ViewPort:cacheUpdate', opts.view || this.view); - - if (!opts.noupdate) { - this.requestContentRefresh(this.currentOffset()); - } - - this.isbusy = false; - }, - - // nowait = (boolean) If true, don't delay before resizing. - // size = (integer) The page size to use instead of auto-determining. - onResize: function(nowait, size) - { - if (!this.opts.content.visible()) { - return; - } - - if (this.resizefunc) { - clearTimeout(this.resizefunc); - } - - if (nowait) { - this._onResize(size); - } else { - this.resizefunc = this._onResize.bind(this, size).delay(0.1); - } - }, - - // size = (integer) The page size to use instead of auto-determining. - _onResize: function(size) - { - var h, - c = this.opts.content, - c_opts = {}, - lh = this._getLineHeight(), - sp = this.split_pane; - - if (size) { - this.page_size = size; - } - - if (this.view && sp.curr != this.pane_mode) { - c_opts.updated = this.createSelection('div', this.visibleRows()).get('domid'); - } - - // Get split pane dimensions - switch (this.pane_mode) { - case 'horiz': - this._initSplitBar(); - - if (!size) { - this.page_size = (sp.horiz.loc && sp.horiz.loc > 0) - ? Math.min(sp.horiz.loc, this.getPageSize('splitmax')) - : this.getPageSize('default'); - } - sp.horiz.loc = this.page_size; - - if (sp.spacer) { - sp.spacer.hide(); - } - - h = lh * this.page_size; - c.setStyle({ height: h + 'px', width: '100%' }); - sp.currbar.show(); - this.opts.pane_data.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show(); - break; - - case 'vert': - this._initSplitBar(); - - if (!size) { - this.page_size = this.getPageSize('max'); - } - - if (!sp.vert.width) { - sp.vert.width = parseInt(this.opts.container.clientWidth * 0.35, 10); - } - - if (sp.spacer) { - sp.spacer.hide(); - } - - h = lh * this.page_size; - c.setStyle({ height: h + 'px', width: sp.vert.width + 'px' }); - sp.currbar.setStyle({ height: h + 'px' }).show(); - this.opts.pane_data.setStyle({ height: h + 'px' }).show(); - break; - - default: - if (sp.curr) { - if (this.pane_mode == 'horiz') { - sp.horiz.loc = this.page_size; - } - [ this.opts.pane_data, sp.currbar ].invoke('hide'); - sp.curr = sp.currbar = null; - } - - if (!size) { - this.page_size = this.getPageSize('max'); - } - - if (sp.spacer) { - sp.spacer.show(); - } else { - sp.spacer = new Element('DIV').setStyle({ clear: 'left' }); - this.opts.content.up().insert(sp.spacer); - } - - c.setStyle({ height: (lh * this.page_size) + 'px', width: '100%' }); - break; - } - - if (this.view) { - this.requestContentRefresh(this.currentOffset(), c_opts); - } - }, - - // offset = (integer) TODO - // opts = (object) See _updateContent() - requestContentRefresh: function(offset, opts) - { - if (!this._updateContent(offset, opts)) { - return false; - } - - var limit = this._getBuffer().isNearingLimit(offset); - if (limit) { - this._fetchBuffer({ - background: true, - nearing: limit, - offset: offset - }); - } - - return true; - }, - - // opts = (object) The following parameters: - // One of the following is REQUIRED: - // offset: (integer) Value of offset - // search: (object) List of search keys/values - // - // OPTIONAL: - // background: (boolean) Do fetch in background - // callback: (function) A callback to run when the request is complete - // initial: (boolean) Is this the initial access to this view? - // nearing: (string) TODO [only used w/offset] - // params: (object) Parameters to add to outgoing URL - // purge: (boolean) If true, purge the current rowlist and rebuild. - // Attempts to reuse the current data cache. - // view: (string) The view to retrieve. Defaults to current view. - _fetchBuffer: function(opts) - { - if (this.isbusy) { - return this._fetchBuffer.bind(this, opts).defer(); - } - - this.isbusy = true; - - var llist, lrows, rlist, tmp, type, value, - view = (opts.view || this.view), - b = this._getBuffer(view), - params = $H(opts.params), - r_id = this.request_num++; - - // Only fire fetch event if we are loading in foreground. - if (!opts.background) { - this.opts.container.fire('ViewPort:fetch', view); - } - - params.update({ requestid: r_id }); - - // Determine if we are querying via offset or a search query - if (opts.search || opts.initial || opts.purge) { - /* If this is an initial request, 'type' will be set correctly - * further down in the code. */ - if (opts.search) { - type = 'search'; - value = opts.search; - params.set('search', Object.toJSON(value)); - } - - if (opts.initial) { - params.set('initial', 1); - } - - if (opts.purge) { - b.resetRowlist(); - } - - tmp = this._lookbehind(); - - params.update({ - after: this.bufferSize() - tmp, - before: tmp - }); - } - - if (!opts.search) { - type = 'rownum'; - value = opts.offset + 1; - - // llist: keys - request_ids; vals - loading rownums - llist = b.getMetaData('llist') || $H(); - lrows = llist.values().flatten(); - - b.setMetaData({ req_offset: opts.offset }, true); - - /* If the current offset is part of a pending request, update - * the offset. */ - if (lrows.size() && - b.sliceLoaded(value, lrows)) { - /* One more hurdle. If we are loading in background, and now - * we are in foreground, we need to search for the request - * that contains the current rownum. For now, just use the - * last request. */ - if (!this.active_req && !opts.background) { - this.active_req = llist.keys().numericSort().last(); - } - this.isbusy = false; - return; - } - - /* This gets the list of rows needed which do not already appear - * in the buffer. */ - tmp = this._getSliceBounds(value, opts.nearing, view); - rlist = $A($R(tmp.start, tmp.end)).diff(b.getAllRows()); - - if (!rlist.size()) { - this.isbusy = false; - return; - } - - /* Add rows to the loading list for the view. */ - rlist = rlist.diff(lrows).numericSort(); - llist.set(r_id, rlist); - b.setMetaData({ llist: llist }, true); - - params.update({ slice: rlist.first() + ':' + rlist.last() }); - } - - if (opts.callback) { - tmp = b.getMetaData('callback') || $H(); - tmp.set(r_id, opts.callback); - b.setMetaData({ callback: tmp }, true); - } - - if (!opts.background) { - this.active_req = r_id; - this._handleWait(); - } - - this._ajaxRequest(params, { noslice: true, view: view }); - - this.isbusy = false; - }, - - // rownum = (integer) Row number - // nearing = (string) 'bottom', 'top', null - // view = (string) ID of view. - _getSliceBounds: function(rownum, nearing, view) - { - var b_size = this.bufferSize(), - ob = {}, trows; - - switch (nearing) { - case 'bottom': - ob.start = rownum + this.getPageSize(); - ob.end = ob.start + b_size; - break; - - case 'top': - ob.start = Math.max(rownum - b_size, 1); - ob.end = rownum; - break; - - default: - ob.start = rownum - this._lookbehind(); - - /* Adjust slice if it runs past edge of available rows. In this - * case, fetching a tiny buffer isn't as useful as switching - * the unused buffer space to the other endpoint. Always allow - * searching past the value of total_rows, since the size of the - * dataset may have increased. */ - trows = this.getMetaData('total_rows', view); - if (trows) { - ob.end = ob.start + b_size; - - if (ob.end > trows) { - ob.start -= ob.end - trows; - } - - if (ob.start < 1) { - ob.end += 1 - ob.start; - ob.start = 1; - } - } else { - ob.start = Math.max(ob.start, 1); - ob.end = ob.start + b_size; - } - break; - } - - return ob; - }, - - _lookbehind: function() - { - return parseInt((this.opts.lookbehind * 0.01) * this.bufferSize(), 10); - }, - - // args = (object) The list of parameters. - // opts = (object) [noslice, view] - // Returns a Hash object - addRequestParams: function(args, opts) - { - opts = opts || {}; - var cid = this.getMetaData('cacheid', opts.view), - cached, params, rowlist; - - params = this.opts.onAjaxRequest - ? this.opts.onAjaxRequest(opts.view || this.view) - : $H(); - - params.update({ view: opts.view || this.view }); - - if (cid) { - params.update({ cacheid: cid }); - } - - if (!opts.noslice) { - rowlist = this._getSliceBounds(this.currentOffset(), null, opts.view); - params.update({ slice: rowlist.start + ':' + rowlist.end }); - } - - if (this.opts.onCachedList) { - cached = this.opts.onCachedList(opts.view || this.view); - } else { - cached = this._getBuffer(opts.view).getAllUIDs(); - cached = cached.size() - ? cached.toJSON() - : ''; - } - - if (cached.length) { - params.update({ cache: cached }); - } - - return params.merge(args); - }, - - // params - (object) A list of parameters to send to server - // opts - (object) Args to pass to addRequestParams(). - _ajaxRequest: function(params, other) - { - new Ajax.Request(this.opts.ajax_url, Object.extend(this.opts.ajax_opts || {}, { - evalJS: false, - evalJSON: true, - onComplete: this.ajax_response, - onFailure: this.opts.onAjaxFailure || Prototype.emptyFunction, - parameters: this.addRequestParams(params, other) - })); - }, - - _ajaxRequestComplete: function(r) - { - if (r.responseJSON) { - this.parseJSONResponse(r.responseJSON); - } - }, - - // r - (object) responseJSON returned from the server. - parseJSONResponse: function(r) - { - if (!r.ViewPort) { - return; - } - - r = r.ViewPort; - - if (r.rangelist) { - this.select(this.createSelection('uid', r.rangelist, r.view)); - this.opts.container.fire('ViewPort:endFetch', r.view); - } - - if (!Object.isUndefined(r.updatecacheid)) { - this._getBuffer(r.view).setMetaData({ cacheid: r.updatecacheid }, true); - } else if (!Object.isUndefined(r.cacheid)) { - this._ajaxResponse(r); - } - }, - - // r = (Object) viewport response object - _ajaxResponse: function(r) - { - if (this.isbusy) { - this._ajaxResponse.bind(this, r).defer(); - return; - } - - this.isbusy = true; - this._clearWait(); - - var callback, offset, tmp, - buffer = this._getBuffer(r.view), - llist = buffer.getMetaData('llist') || $H(), - updated = []; - - buffer.update(Object.isArray(r.data) ? {} : r.data, Object.isArray(r.rowlist) ? {} : r.rowlist, r.metadata || {}, { reset: r.reset, resetmd: r.resetmd, update: r.update }); - - if (r.reset) { - this.select(new ViewPort_Selection()); - } else if (r.update && r.disappear && r.disappear.size()) { - this.deselect(this.createSelection('uid', r.disappear, r.view)); - buffer.removeData(r.disappear); - } - - llist.unset(r.requestid); - - tmp = { - cacheid: r.cacheid, - llist: llist, - total_rows: r.totalrows - }; - if (r.label) { - tmp.label = r.label; - } - buffer.setMetaData(tmp, true); - - this.opts.container.fire('ViewPort:cacheUpdate', r.view); - - if (r.requestid && - r.requestid == this.active_req) { - this.active_req = null; - callback = buffer.getMetaData('callback'); - offset = buffer.getMetaData('req_offset'); - - if (callback && callback.get(r.requestid)) { - callback.get(r.requestid)(r); - callback.unset(r.requestid); - } - - buffer.setMetaData({ callback: undefined, req_offset: undefined }, true); - - this.opts.container.fire('ViewPort:endFetch', r.view); - } - - if (this.view == r.view) { - if (r.update) { - updated = this.createSelection('uid', Object.keys(r.data)).get('domid'); - } - this._updateContent(Object.isUndefined(r.rownum) ? (Object.isUndefined(offset) ? this.currentOffset() : offset) : Number(r.rownum) - 1, { updated: updated }); - } else if (r.rownum) { - // We loaded in the background. If rownumber information was - // provided, we need to save this or else we will position the - // viewport incorrectly. - buffer.setMetaData({ offset: Number(r.rownum) - 1 }, true); - } - - this.isbusy = false; - }, - - // offset = (integer) TODO - // opts = (object) TODO [background, updated, view] - _updateContent: function(offset, opts) - { - opts = opts || {}; - - if (!this._getBuffer(opts.view).sliceLoaded(offset)) { - opts.offset = offset; - this._fetchBuffer(opts); - return false; - } - - var added = {}, - c = this.opts.content, - page_size = this.getPageSize(), - tmp = [], - updated = opts.updated || [], - vr = this.visibleRows(), - fdiv, rows; - - this.scroller.setSize(page_size, this.getMetaData('total_rows')); - this.scrollTo(offset + 1, { noupdate: true, top: true }); - - offset = this.currentOffset(); - if (this.opts.onContentOffset) { - offset = this.opts.onContentOffset(offset); - } - - rows = this.createSelection('rownum', $A($R(offset + 1, offset + page_size))); - - if (rows.size()) { - fdiv = document.createDocumentFragment().appendChild(new Element('DIV')); - - rows.get('dataob').each(function(r) { - var elt; - if (!updated.include(r.VP_domid) && - (elt = $(r.VP_domid))) { - tmp.push(elt); - } else { - fdiv.insert({ top: this.prepareRow(r) }); - added[r.VP_domid] = 1; - tmp.push(fdiv.down()); - } - }, this); - - vr.pluck('id').diff(rows.get('domid')).each($).compact().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); - - c.childElements().invoke('remove'); - - tmp.each(function(r) { - c.insert(r); - if (added[r.identify()]) { - this.opts.container.fire('ViewPort:add', r); - } - }, this); - } else { - vr.each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); - vr.invoke('remove'); - c.update(this.empty_msg); - } - - this.scroller.updateDisplay(); - this.opts.container.fire('ViewPort:contentComplete'); - - return true; - }, - - prepareRow: function(row) - { - var r = Object.clone(row); - - r.VP_bg = this.getSelected().contains('uid', r.VP_id) - ? [ 'vpRowSelected' ] - : []; - - return this.opts.onContent(r, this.pane_mode); - }, - - updateRow: function(row) - { - var d = $(row.VP_domid); - if (d) { - this.opts.container.fire('ViewPort:clear', d); - d.replace(this.prepareRow(row)); - this.opts.container.fire('ViewPort:add', $(row.VP_domid)); - } - }, - - _handleWait: function(call) - { - this._clearWait(); - - // Server did not respond in defined amount of time. Alert the - // callback function and set the next timeout. - if (call) { - this.opts.container.fire('ViewPort:wait', this.view); - } - - // Call wait handler every x seconds - if (this.opts.viewport_wait) { - this.waitHandler = this._handleWait.bind(this, true).delay(this.opts.viewport_wait); - } - }, - - _clearWait: function() - { - if (this.waitHandler) { - clearTimeout(this.waitHandler); - this.waitHandler = null; - } - }, - - visibleRows: function() - { - return this.opts.content.select('DIV.vpRow'); - }, - - getMetaData: function(id, view) - { - return this._getBuffer(view).getMetaData(id); - }, - - setMetaData: function(vals, view) - { - this._getBuffer(view).setMetaData(vals, false); - }, - - _getBuffer: function(view, create) - { - view = view || this.view; - - return (!create && this.views[view]) - ? this.views[view] - : new ViewPort_Buffer(this, view); - }, - - currentOffset: function() - { - return this.scroller.currentOffset(); - }, - - // return: (object) The current viewable range of the viewport. - // first: Top-most row offset - // last: Bottom-most row offset - currentViewableRange: function() - { - var offset = this.currentOffset(); - return { - first: offset + 1, - last: Math.min(offset + this.getPageSize(), this.getMetaData('total_rows')) - }; - }, - - _getLineHeight: function() - { - var mode = this.pane_mode || 'horiz'; - - if (!this.split_pane[mode].lh) { - // To avoid hardcoding the line height, create a temporary row to - // figure out what the CSS says. - var d = new Element('DIV', { className: this.opts.list_class }).insert(this.prepareRow({ VP_domid: null }, mode)).hide(); - $(document.body).insert(d); - this.split_pane[mode].lh = d.getHeight(); - d.remove(); - } - - return this.split_pane[mode].lh; - }, - - // (type) = (string) [null (DEFAULT), 'current', 'default', 'max'] - // return: (integer) Number of rows in current view. - getPageSize: function(type) - { - switch (type) { - case 'current': - return Math.min(this.page_size, this.getMetaData('total_rows')); - - case 'default': - return (this.pane_mode == 'vert') - ? this.getPageSize('max') - : Math.max(parseInt(this.getPageSize('max') * 0.45, 10), 5); - - case 'max': - case 'splitmax': - return parseInt(this._getMaxHeight() / this._getLineHeight()) - (type == 'max' ? 0 : 1); - - default: - return this.page_size; - } - }, - - _getMaxHeight: function() - { - return document.viewport.getHeight() - this.opts.content.viewportOffset()[1]; - }, - - bufferSize: function() - { - // Buffer size must be at least the maximum page size. - return Math.round(Math.max(this.getPageSize('max') + 1, this.opts.buffer_pages * this.getPageSize())); - }, - - limitTolerance: function() - { - return Math.round(this.bufferSize() * (this.opts.limit_factor / 100)); - }, - - // mode = (string) Either 'horiz', 'vert', or empty. - showSplitPane: function(mode) - { - this.pane_mode = mode; - this.onResize(true); - }, - - _initSplitBar: function() - { - var sp = this.split_pane; - - if (sp.currbar) { - sp.currbar.hide(); - } - - sp.curr = this.pane_mode; - - if (sp[this.pane_mode].bar) { - sp.currbar = sp[this.pane_mode].bar.show(); - return; - } - - sp.currbar = sp[this.pane_mode].bar = new Element('DIV', { className: this.opts.split_bar_class[this.pane_mode] }); - - if (!this.opts.pane_data.descendantOf(this.opts.container)) { - this.opts.container.insert(this.opts.pane_data.remove()); - } - - this.opts.pane_data.insert({ before: sp.currbar }); - - switch (this.pane_mode) { - case 'horiz': - new Drag(sp.currbar.setStyle({ clear: 'left' }), { - constraint: 'vertical', - ghosting: true, - nodrop: true, - snap: function(x, y, elt) { - var sp = this.split_pane, - l = parseInt((y - sp.pos) / sp.lh); - if (l < 1) { - l = 1; - } else if (l > sp.max) { - l = sp.max; - } - sp.lines = l; - return [ x, sp.pos + (l * sp.lh) ]; - }.bind(this) - }); - break; - - case 'vert': - new Drag(sp.currbar.setStyle({ float: 'left' }), { - constraint: 'horizontal', - ghosting: true, - nodrop: true, - snapToParent: true - }); - break; - } - - if (!sp.init) { - document.observe('DragDrop2:end', this._onDragEnd.bindAsEventListener(this)); - document.observe('DragDrop2:start', this._onDragStart.bindAsEventListener(this)); - document.observe('dblclick', this._onDragDblClick.bindAsEventListener(this)); - sp.init = true; - } - }, - - _onDragStart: function(e) - { - var sp = this.split_pane; - - if (e.element() != sp.currbar) { - return; - } - - if (this.pane_mode == 'horiz') { - // Cache these values since we will be using them multiple - // times in snap(). - sp.lh = this._getLineHeight(); - sp.lines = this.page_size; - sp.max = this.getPageSize('splitmax'); - sp.orig = this.page_size; - sp.pos = this.opts.content.positionedOffset()[1]; - } - - this.opts.container.fire('ViewPort:splitBarStart', this.pane_mode); - }, - - _onDragEnd: function(e) - { - var change, drag, - sp = this.split_pane; - - if (e.element() != sp.currbar) { - return; - } - - switch (this.pane_mode) { - case 'horiz': - this.onResize(true, sp.lines); - change = (sp.orig != sp.lines); - break; - - case 'vert': - drag = DragDrop.Drags.getDrag(e.element()); - sp.vert.width = drag.lastCoord[0]; - this.opts.content.setStyle({ width: sp.vert.width + 'px' }); - change = drag.wasDragged; - break; - } - - if (change) { - this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode); - } - this.opts.container.fire('ViewPort:splitBarEnd', this.pane_mode); - }, - - _onDragDblClick: function(e) - { - if (e.element() != this.split_pane.currbar) { - return; - } - - var change, old_size = this.page_size; - - switch (this.pane_mode) { - case 'horiz': - this.onResize(true, this.getPageSize('default')); - change = (old_size != this.page_size); - break; - - case 'vert': - this.opts.content.setStyle({ width: parseInt(this.opts.container.clientWidth * 0.45, 10) + 'px' }); - change = true; - } - - if (change) { - this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode); - } - }, - - getAllRows: function(view) - { - var buffer = this._getBuffer(view); - return buffer - ? buffer.getAllRows() - : []; - }, - - createSelection: function(format, data, view) - { - var buffer = this._getBuffer(view); - return buffer - ? new ViewPort_Selection(buffer, format, data) - : new ViewPort_Selection(this._getBuffer(this.view)); - }, - - getSelection: function(view) - { - var buffer = this._getBuffer(view); - return this.createSelection('uid', buffer ? buffer.getSelected().get('uid') : [], view); - }, - - // vs = (Viewport_Selection | array) A Viewport_Selection object -or- if - // opts.range is set, an array of row numbers. - // opts = (object) TODO [add, range, search] - select: function(vs, opts) - { - opts = opts || {}; - - var b = this._getBuffer(), - sel, slice; - - if (opts.search) { - return this._fetchBuffer({ - callback: function(r) { - if (r.rownum) { - this.select(this.createSelection('rownum', [ r.rownum ]), { add: opts.add, range: opts.range }); - } - }.bind(this), - search: opts.search - }); - } - - if (opts.range) { - slice = this.createSelection('rownum', vs); - if (vs.size() != slice.size()) { - this.opts.container.fire('ViewPort:fetch', this.view); - return this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() }); - } - vs = slice; - } - - if (!opts.add) { - sel = this.getSelected(); - b.deselect(sel, true); - sel.get('div').invoke('removeClassName', 'vpRowSelected'); - } - b.select(vs); - vs.get('div').invoke('addClassName', 'vpRowSelected'); - this.opts.container.fire('ViewPort:select', { opts: opts, vs: vs }); - }, - - // vs = (Viewport_Selection) A Viewport_Selection object. - // opts = (object) TODO [clearall] - deselect: function(vs, opts) - { - opts = opts || {}; - - if (vs.size() && - this._getBuffer().deselect(vs, opts && opts.clearall)) { - vs.get('div').invoke('removeClassName', 'vpRowSelected'); - this.opts.container.fire('ViewPort:deselect', { opts: opts, vs: vs }); - } - }, - - getSelected: function() - { - return Object.clone(this._getBuffer().getSelected()); - } - -}), - -/** - * ViewPort_Scroller - */ -ViewPort_Scroller = Class.create({ - // Variables initialized to undefined: - // noupdate, scrollDiv, scrollbar, vertscroll, vp - - initialize: function(vp) - { - this.vp = vp; - }, - - _createScrollBar: function() - { - if (this.scrollDiv) { - return; - } - - var c = this.vp.opts.content; - - // Create the outer div. - this.scrollDiv = new Element('DIV', { className: 'vpScroll' }).setStyle({ float: 'left', overflow: 'hidden' }).hide(); - c.insert({ after: this.scrollDiv }); - - this.scrollDiv.observe('Slider2:change', this._onScroll.bind(this)); - if (this.vp.opts.onSlide) { - this.scrollDiv.observe('Slider2:slide', this.vp.opts.onSlide); - } - - // Create scrollbar object. - this.scrollbar = new Slider2(this.scrollDiv, { - buttonclass: { up: 'vpScrollUp', down: 'vpScrollDown' }, - cursorclass: 'vpScrollCursor', - pagesize: this.vp.getPageSize(), - totalsize: this.vp.getMetaData('total_rows') - }); - - // Mouse wheel handler. - c.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', function(e) { - var move_num = Math.min(this.vp.getPageSize(), 3); - this.moveScroll(this.currentOffset() + ((e.wheelDelta >= 0 || e.detail < 0) ? (-1 * move_num) : move_num)); - /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818 - * Need to stop or else multiple scroll events may be fired. We - * lose the ability to have the mousescroll bubble up, but that is - * more desirable than having the wrong scrolling behavior. */ - if (Prototype.Browser.Gecko && !e.stop) { - Event.stop(e); - } - }.bindAsEventListener(this)); - }, - - setSize: function(viewsize, totalsize) - { - this._createScrollBar(); - this.scrollbar.setHandleLength(viewsize, totalsize); - }, - - updateDisplay: function() - { - var c = this.vp.opts.content, - vs = false; - - if (this.scrollbar.needScroll()) { - switch (this.vp.pane_mode) { - case 'vert': - this.scrollDiv.setStyle({ marginLeft: 0 }); - if (!this.vertscroll) { - c.setStyle({ width: (c.clientWidth - this.scrollDiv.getWidth()) + 'px' }); - } - vs = true; - break; - - case 'horiz': - default: - this.scrollDiv.setStyle({ marginLeft: '-' + this.scrollDiv.getWidth() + 'px' }); - break; - } - - this.scrollDiv.setStyle({ height: c.clientHeight + 'px' }); - } else if ((this.vp.pane_mode == 'vert') && this.vertscroll) { - c.setStyle({ width: (c.clientWidth + this.scrollDiv.getWidth()) + 'px' }); - } - - this.vertscroll = vs; - this.scrollbar.updateHandleLength(); - }, - - clear: function() - { - this.setSize(0, 0); - this.scrollbar.updateHandleLength(); - }, - - // offset = (integer) Offset to move the scrollbar to - moveScroll: function(offset) - { - this._createScrollBar(); - this.scrollbar.setScrollPosition(offset); - }, - - _onScroll: function() - { - if (!this.noupdate) { - this.vp.requestContentRefresh(this.currentOffset()); - } - }, - - currentOffset: function() - { - return this.scrollbar ? this.scrollbar.getValue() : 0; - } - -}), - -/** - * ViewPort_Buffer - * - * Note: recognize the difference between offset (current location in the - * viewport - starts at 0) with start parameters (the row numbers - starts - * at 1). - */ -ViewPort_Buffer = Class.create({ - - initialize: function(vp, view) - { - this.vp = vp; - this.view = view; - this.clear(); - }, - - getView: function() - { - return this.view; - }, - - // d = (object) Data - // l = (object) Rowlist - // md = (object) User defined metadata - // opts = (object) TODO [reset, resetmd, update] - update: function(d, l, md, opts) - { - d = $H(d); - l = $H(l); - opts = opts || {}; - - if (!opts.reset && this.data.size()) { - this.data.update(d); - } else { - this.data = d; - } - - if (opts.update || opts.reset) { - this.uidlist = l; - this.rowlist = $H(); - } else { - this.uidlist = this.uidlist.size() ? this.uidlist.merge(l) : l; - } - - l.each(function(o) { - this.rowlist.set(o.value, o.key); - }, this); - - if (opts.resetmd) { - this.usermdata = $H(md); - } else { - $H(md).each(function(pair) { - if (Object.isString(pair.value) || - Object.isNumber(pair.value) || - Object.isArray(pair.value)) { - this.usermdata.set(pair.key, pair.value); - } else { - var val = this.usermdata.get(pair.key); - if (val) { - this.usermdata.get(pair.key).update($H(pair.value)); - } else { - this.usermdata.set(pair.key, $H(pair.value)); - } - } - }, this); - } - }, - - // offset = (integer) Offset of the beginning of the slice. - // rows = (array) Additional rows to include in the search. - sliceLoaded: function(offset, rows) - { - var range, tr = this.getMetaData('total_rows'); - - // Undefined here indicates we have never sent a previous buffer - // request. - if (Object.isUndefined(tr)) { - return false; - } - - range = $A($R(offset + 1, Math.min(offset + this.vp.getPageSize() - 1, tr))); - - return rows - ? (range.diff(this.rowlist.keys().concat(rows)).size() == 0) - : !this._rangeCheck(range); - }, - - isNearingLimit: function(offset) - { - if (this.uidlist.size() != this.getMetaData('total_rows')) { - if (offset != 0 && - this._rangeCheck($A($R(Math.max(offset + 1 - this.vp.limitTolerance(), 1), offset)))) { - return 'top'; - } else if (this._rangeCheck($A($R(offset + 1, Math.min(offset + this.vp.limitTolerance() + this.vp.getPageSize() - 1, this.getMetaData('total_rows')))).reverse())) { - // Search for missing rows in reverse order since in normal - // usage (sequential scrolling through the row list) rows are - // more likely to be missing at furthest from the current - // view. - return 'bottom'; - } - } - }, - - _rangeCheck: function(range) - { - return !range.all(this.rowlist.get.bind(this.rowlist)); - }, - - getData: function(uids) - { - return uids.collect(function(u) { - var e = this.data.get(u); - if (!Object.isUndefined(e)) { - // We can directly write the rownum to the original object - // since we will always rewrite when creating rows. - e.VP_domid = 'VProw' + this.view + '_' + u; - e.VP_rownum = this.uidlist.get(u); - e.VP_id = u; - return e; - } - }, this).compact(); - }, - - getAllUIDs: function() - { - return this.uidlist.keys(); - }, - - getAllRows: function() - { - return this.rowlist.keys(); - }, - - rowsToUIDs: function(rows) - { - return rows.collect(this.rowlist.get.bind(this.rowlist)).compact(); - }, - - // vs = (Viewport_Selection) TODO - select: function(vs) - { - this.selected.add('uid', vs.get('uid')); - }, - - // vs = (Viewport_Selection) TODO - // clearall = (boolean) Clear all entries? - deselect: function(vs, clearall) - { - var size = this.selected.size(); - - if (clearall) { - this.selected.clear(); - } else { - this.selected.remove('uid', vs.get('uid')); - } - return size != this.selected.size(); - }, - - getSelected: function() - { - return this.selected; - }, - - // rownums = (array) Array of row numbers to remove. - remove: function(rownums) - { - var minrow = rownums.min(), - rowsize = this.rowlist.size(), - rowsubtract = 0, - newsize = rowsize - rownums.size(); - - return this.rowlist.keys().each(function(n) { - if (n >= minrow) { - var id = this.rowlist.get(n), r; - if (rownums.include(n)) { - this.data.unset(id); - this.uidlist.unset(id); - rowsubtract++; - } else if (rowsubtract) { - r = n - rowsubtract; - this.rowlist.set(r, id); - this.uidlist.set(id, r); - } - if (n > newsize) { - this.rowlist.unset(n); - } - } - }, this); - }, - - removeData: function(uids) - { - uids.each(function(u) { - this.data.unset(u); - this.uidlist.unset(u); - }, this); - }, - - resetRowlist: function() - { - this.rowlist = $H(); - }, - - clear: function() - { - this.data = $H(); - this.mdata = $H({ total_rows: 0 }); - this.rowlist = $H(); - this.selected = new ViewPort_Selection(this); - this.uidlist = $H(); - this.usermdata = $H(); - }, - - getMetaData: function(id) - { - var data = this.mdata.get(id); - - return Object.isUndefined(data) - ? this.usermdata.get(id) - : data; - }, - - setMetaData: function(vals, priv) - { - if (priv) { - this.mdata.update(vals); - } else { - this.usermdata.update(vals); - } - } - -}), - -/** - * ViewPort_Selection - */ -ViewPort_Selection = Class.create({ - - // Define property to aid in object detection - viewport_selection: true, - - // Formats: - // 'dataob' = Data objects - // 'div' = DOM DIVs - // 'domid' = DOM IDs - // 'rownum' = Row numbers - // 'uid' = Unique IDs - initialize: function(buffer, format, data) - { - this.buffer = buffer; - this.clear(); - if (!Object.isUndefined(format)) { - this.add(format, data); - } - }, - - add: function(format, d) - { - var c = this._convert(format, d); - this.data = this.data.size() ? this.data.concat(c).uniq() : c; - }, - - remove: function(format, d) - { - this.data = this.data.diff(this._convert(format, d)); - }, - - _convert: function(format, d) - { - d = Object.isArray(d) ? d : [ d ]; - - // Data is stored internally as UIDs. - switch (format) { - case 'dataob': - return d.pluck('VP_id'); - - case 'div': - // ID here is the DOM ID of the element object. - d = d.pluck('id'); - // Fall-through - - case 'domid': - return d.invoke('substring', 6 + this.buffer.getView().length); - - case 'rownum': - return this.buffer.rowsToUIDs(d); - - case 'uid': - return d; - } - }, - - clear: function() - { - this.data = []; - }, - - get: function(format) - { - format = Object.isUndefined(format) ? 'uid' : format; - if (format == 'uid') { - return this.data; - } - var d = this.buffer.getData(this.data); - - switch (format) { - case 'dataob': - return d; - - case 'div': - return d.pluck('VP_domid').collect(function(e) { return $(e); }).compact(); - - case 'domid': - return d.pluck('VP_domid'); - - case 'rownum': - return d.pluck('VP_rownum'); - } - }, - - contains: function(format, d) - { - return this.data.include(this._convert(format, d).first()); - }, - - // params = (Object) Key is search key, value is object -> key of object - // must be the following: - // equal - Matches any value contained in the query array. - // include - Matches if this value is contained within the array. - // notequal - Matches any value not contained in the query array. - // notinclude - Matches if this value is not contained within the array. - // regex - Matches the RegExp contained in the query. - search: function(params) - { - return new ViewPort_Selection(this.buffer, 'uid', this.get('dataob').findAll(function(i) { - // i = data object - return $H(params).all(function(k) { - // k.key = search key; k.value = search criteria - return $H(k.value).all(function(s) { - // s.key = search type; s.value = search query - switch (s.key) { - case 'equal': - case 'notequal': - var r = i[k.key] && s.value.include(i[k.key]); - return (s.key == 'equal') ? r : !r; - - case 'include': - case 'notinclude': - var r = i[k.key] && Object.isArray(i[k.key]) && i[k.key].include(s.value); - return (s.key == 'include') ? r : !r; - - case 'regex': - return i[k.key].match(s.value); - } - }); - }); - }).pluck('VP_id')); - }, - - size: function() - { - return this.data.size(); - }, - - set: function(vals) - { - this.get('dataob').each(function(d) { - $H(vals).each(function(v) { - d[v.key] = v.value; - }); - }); - }, - - getBuffer: function() - { - return this.buffer; - } - -}); - -/** Utility Functions **/ -Object.extend(Array.prototype, { - // Need our own diff() function because prototypejs's without() function - // does not handle array input. - diff: function(values) - { - return this.select(function(value) { - return !values.include(value); - }); - }, - numericSort: function() - { - return this.collect(Number).sort(function(a, b) { - return (a > b) ? 1 : ((a < b) ? -1 : 0); - }); - } -}); diff --git a/imp/js/dimpbase.js b/imp/js/dimpbase.js new file mode 100644 index 000000000..bccdd4a65 --- /dev/null +++ b/imp/js/dimpbase.js @@ -0,0 +1,3143 @@ +/** + * dimpbase.js - Javascript used in the base DIMP page. + * + * Copyright 2005-2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + */ + +var DimpBase = { + // Vars used and defaulting to null/false: + // cfolderaction, folder, folderswitch, pollPE, pp, preview_replace, + // resize, rownum, search, splitbar, template, uid, viewport + // msglist_template_horiz and msglist_template_vert set via + // js/mailbox-dimp.js + cacheids: {}, + lastrow: -1, + pivotrow: -1, + ppcache: {}, + ppfifo: [], + showunsub: 0, + tcache: {}, + + // Preview pane cache size is 20 entries. Given that a reasonable guess + // of an average e-mail size is 10 KB (including headers), also make + // an estimate that the JSON data size will be approx. 10 KB. 200 KB + // should be a fairly safe caching value for any recent browser. + ppcachesize: 20, + + // Message selection functions + + // id = (string) DOM ID + // opts = (Object) Boolean options [ctrl, right, shift] + msgSelect: function(id, opts) + { + var bounds, + row = this.viewport.createSelection('domid', id), + rownum = row.get('rownum').first(), + sel = this.isSelected('domid', id), + selcount = this.selectedCount(); + + this.lastrow = rownum; + + // Some browsers need to stop the mousedown event before it propogates + // down to the browser level in order to prevent text selection on + // drag/drop actions. Clicking on a message should always lose focus + // from the search input, because the user may immediately start + // keyboard navigation after that. Thus, we need to ensure that a + // message click loses focus on the search input. + if ($('qsearch')) { + $('qsearch_input').blur(); + } + + if (opts.shift) { + if (selcount) { + if (!sel || selcount != 1) { + bounds = [ rownum, this.pivotrow ]; + this.viewport.select($A($R(bounds.min(), bounds.max())), { range: true }); + } + return; + } + } else if (opts.ctrl) { + this.pivotrow = rownum; + if (sel) { + this.viewport.deselect(row, { right: opts.right }); + return; + } else if (opts.right || selcount) { + this.viewport.select(row, { add: true, right: opts.right }); + return; + } + } + + this.viewport.select(row, { right: opts.right }); + }, + + selectAll: function() + { + this.viewport.select(this.viewport.getAllRows(), { range: true }); + }, + + isSelected: function(format, data) + { + return this.viewport.getSelected().contains(format, data); + }, + + selectedCount: function() + { + return (this.viewport) ? this.viewport.getSelected().size() : 0; + }, + + resetSelected: function() + { + if (this.viewport) { + this.viewport.deselect(this.viewport.getSelected(), { clearall: true }); + } + this.toggleButtons(); + this.clearPreviewPane(); + }, + + // num = (integer) See absolute. + // absolute = Is num an absolute row number - from 1 -> page_size (true) - + // or a relative change from the current selected value (false) + // If no current selected value, the first message in the + // current viewport is selected. + moveSelected: function(num, absolute) + { + var curr, curr_row, row, row_data, sel; + + if (absolute) { + if (!this.viewport.getMetaData('total_rows')) { + return; + } + curr = num; + } else { + if (num == 0) { + return; + } + + sel = this.viewport.getSelected(); + switch (sel.size()) { + case 0: + curr = this.viewport.currentOffset(); + curr += (num > 0) ? 1 : this.viewport.getPageSize('current'); + break; + + case 1: + curr_row = sel.get('dataob').first(); + curr = curr_row.VP_rownum + num; + break; + + default: + sel = sel.get('rownum'); + curr = (num > 0 ? sel.max() : sel.min()) + num; + break; + } + curr = (num > 0) ? Math.min(curr, this.viewport.getMetaData('total_rows')) : Math.max(curr, 1); + } + + row = this.viewport.createSelection('rownum', curr); + if (row.size()) { + row_data = row.get('dataob').first(); + if (!curr_row || row_data.imapuid != curr_row.imapuid) { + this.viewport.scrollTo(row_data.VP_rownum); + this.viewport.select(row, { delay: 0.3 }); + } + } else { + this.rownum = curr; + this.viewport.requestContentRefresh(curr - 1); + } + }, + // End message selection functions + + go: function(loc, data) + { + var app, f, separator; + + /* If switching from options, we need to reload page to pick up any + * prefs changes. */ + if (this.folder === null && + loc != 'options' && + $('appoptions') && + $('appoptions').hasClassName('on')) { + $('dimpPage').hide(); + $('dimpLoading').show(); + return DimpCore.redirect(DIMP.conf.URI_DIMP + '#' + loc, true); + } + + if (loc.startsWith('compose:')) { + return; + } + + if (loc.startsWith('msg:')) { + separator = loc.indexOf(':', 4); + f = loc.substring(4, separator); + this.uid = parseInt(loc.substring(separator + 1), 10); + loc = 'folder:' + f; + // Now fall through to the 'folder:' check below. + } + + if (loc.startsWith('folder:')) { + f = loc.substring(7); + if (this.folder != f || !$('dimpmain_folder').visible()) { + this.highlightSidebar(this.getFolderId(f)); + if (!$('dimpmain_folder').visible()) { + $('dimpmain_portal').hide(); + $('dimpmain_folder').show(); + } + + // This catches the refresh case - no need to re-add to history + if (!Object.isUndefined(this.folder) && !this.search) { + location.hash = encodeURIComponent(loc); + } + } + + this.loadMailbox(f); + return; + } + + f = this.folder; + this.folder = null; + $('dimpmain_folder').hide(); + $('dimpmain_portal').update(DIMP.text.loading).show(); + + if (loc.startsWith('app:')) { + app = loc.substr(4); + if (app == 'imp') { + this.go('folder:INBOX'); + return; + } + this.highlightSidebar('app' + app); + location.hash = encodeURIComponent(loc); + if (data) { + this.iframeContent(loc, data); + } else if (DIMP.conf.app_urls[app]) { + this.iframeContent(loc, DIMP.conf.app_urls[app]); + } + return; + } + + switch (loc) { + case 'search': + // data: 'edit_query' = folder to edit; otherwise, loads search + // screen with current mailbox as default search mailbox + if (!data) { + data = { search_mailbox: f }; + } + this.highlightSidebar(); + DimpCore.setTitle(DIMP.text.search); + this.iframeContent(loc, DimpCore.addURLParam(DIMP.conf.URI_SEARCH, data)); + break; + + case 'portal': + this.highlightSidebar('appportal'); + location.hash = encodeURIComponent(loc); + DimpCore.setTitle(DIMP.text.portal); + DimpCore.doAction('showPortal', {}, { callback: this._portalCallback.bind(this) }); + break; + + case 'options': + this.highlightSidebar('appoptions'); + location.hash = encodeURIComponent(loc); + DimpCore.setTitle(DIMP.text.prefs); + this.iframeContent(loc, DIMP.conf.URI_PREFS_IMP); + break; + } + }, + + highlightSidebar: function(id) + { + // Folder bar may not be fully loaded yet. + if ($('foldersLoading').visible()) { + this.highlightSidebar.bind(this, id).defer(); + return; + } + + var curr = $('sidebar').down('.on'), + elt = $(id); + + if (curr == elt) { + return; + } + + if (elt && !elt.match('LI')) { + elt = elt.up(); + if (!elt) { + return; + } + } + + if (curr) { + curr.removeClassName('on'); + } + + if (elt) { + elt.addClassName('on'); + this._toggleSubFolder(elt, 'exp'); + } + }, + + iframeContent: function(name, loc) + { + var container = $('dimpmain_portal'), iframe; + if (!container) { + DimpCore.showNotifications([ { type: 'horde.error', message: 'Bad portal!' } ]); + return; + } + + iframe = new Element('IFRAME', { id: 'iframe' + (name === null ? loc : name), className: 'iframe', frameBorder: 0, src: loc }).setStyle({ height: document.viewport.getHeight() + 'px' }); + container.insert(iframe); + }, + + // r = ViewPort row data + msgWindow: function(r) + { + this.updateSeenUID(r, 1); + var url = DIMP.conf.URI_MESSAGE; + url += (url.include('?') ? '&' : '?') + + $H({ folder: r.view, + uid: Number(r.imapuid) }).toQueryString(); + DimpCore.popupWindow(url, 'msgview' + r.view + r.imapuid); + }, + + composeMailbox: function(type) + { + var sel = this.viewport.getSelected(); + if (!sel.size()) { + return; + } + sel.get('dataob').each(function(s) { + DimpCore.compose(type, { folder: s.view, uid: s.imapuid }); + }); + }, + + loadMailbox: function(f, opts) + { + var need_delete; + opts = opts || {}; + + if (!this.viewport) { + this._createViewPort(); + } + + if (!opts.background) { + this.resetSelected(); + this.quicksearchClear(true); + + if (this.folder != f) { + $('folderName').update(DIMP.text.loading); + $('msgHeader').update(); + this.folderswitch = true; + + /* Don't cache results of search folders - since we will need + * to grab new copy if we ever return to it. */ + if (this.isSearch(this.folder)) { + need_delete = this.folder; + } + + this.folder = f; + + if (this.isSearch(f)) { + if (!this.search || this.search.flag) { + this._quicksearchDeactivate(!this.search); + } + $('refreshlink').show(); + } else { + $('refreshlink').hide(); + } + } + } + + this.viewport.loadView(f, { search: (this.uid ? { imapuid: Number(this.uid) } : null), background: opts.background}); + + if (need_delete) { + this.viewport.deleteView(need_delete); + } + }, + + _createViewPort: function() + { + var container = $('msgSplitPane'); + + [ $('msglistHeader') ].invoke(DIMP.conf.preview_pref == 'vert' ? 'hide' : 'show'); + + this.template = { + horiz: new Template(this.msglist_template_horiz), + vert: new Template(this.msglist_template_vert) + }; + + this.viewport = new ViewPort({ + // Mandatory config + ajax_url: DIMP.conf.URI_AJAX + 'viewPort', + container: container, + onContent: function(r, mode) { + var bg, re, u, + thread = $H(this.viewport.getMetaData('thread')), + tsort = (this.viewport.getMetaData('sortby') == $H(DIMP.conf.sort).get('thread').v); + + r.subjectdata = r.status = ''; + r.subjecttitle = r.subject; + + // Add thread graphics + if (tsort) { + u = thread.get(r.imapuid); + if (u) { + $R(0, u.length, true).each(function(i) { + var c = u.charAt(i); + if (!this.tcache[c]) { + this.tcache[c] = ''; + } + 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 = '' + ptr.l.truncate(10) + ''; + } + r.subjectdata += ptr.elt; + } else { + if (!ptr.elt) { + ptr.elt = '
    '; + } + 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, '#{1}'); + }); + } + + // 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
      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 ul is created if necessary. + if (!ob.s && ob.ch) { + li.insert({ after: new Element('LI', { className: 'subfolders', id: submboxid }).insert(new Element('UL')).hide() }); + } + } + + li.store('ftype', ftype); + + // Make the new folder a drop target. + if (!ob.v) { + new Drop(li, this._folderDropConfig); + } + + // Check for unseen messages + if (ob.po) { + li.store('u', ''); + this.setFolderLabel(mbox, ob.u); + } + + switch (ftype) { + case 'special': + // For purposes of the contextmenu, treat special folders + // like regular folders. + ftype = 'folder'; + // Fall through. + + case 'container': + case 'folder': + new Drag(li, this._folderDragConfig); + DimpCore.addContextMenu({ + id: fid, + type: ftype + }); + break; + + case 'scontainer': + case 'virtual': + DimpCore.addContextMenu({ + id: fid, + type: (ob.v == 2) ? 'vfolder' : 'noactions' + }); + break; + } + }, + + deleteFolder: function(folder) + { + if (this.folder == folder) { + this.go('folder:INBOX'); + } + this.deleteFolderElt(this.getFolderId(folder), true); + }, + + changeFolder: function(ob) + { + var fdiv, oldexpand, + fid = this.getFolderId(ob.m); + + if ($(fid + '_special')) { + // The case of children being added to a special folder is + // handled by createFolder(). + if (!ob.ch) { + this.deleteFolderElt(fid + '_special', true); + } + return; + } + + fdiv = $(fid).down('DIV'); + oldexpand = fdiv && fdiv.hasClassName('col'); + + this.deleteFolderElt(fid, !ob.ch); + if (ob.co && this.folder == ob.m) { + this.go('folder:INBOX'); + } + this.createFolder(ob); + if (ob.ch && oldexpand) { + fdiv.removeClassName('exp').addClassName('col'); + } + }, + + deleteFolderElt: function(fid, sub) + { + var f = $(fid), submbox; + if (!f) { + return; + } + + if (sub) { + submbox = $(this.getSubFolderId(fid)); + if (submbox) { + submbox.remove(); + } + } + [ DragDrop.Drags.getDrag(fid), DragDrop.Drops.getDrop(fid) ].compact().invoke('destroy'); + this._removeMouseEvents(f); + if (this.viewport) { + this.viewport.deleteView(fid); + } + f.remove(); + }, + + _sizeFolderlist: function() + { + var nf = $('normalfolders'); + nf.setStyle({ height: (document.viewport.getHeight() - nf.cumulativeOffset()[1]) + 'px' }); + }, + + toggleSubscribed: function() + { + this.showunsub = !this.showunsub; + $('ctx_folderopts_sub', 'ctx_folderopts_unsub').invoke('toggle'); + this._reloadFolders(); + }, + + _reloadFolders: function() + { + $('foldersLoading').show(); + $('foldersSidebar').hide(); + + [ $('specialfolders').childElements(), $('dropbase').nextSiblings() ].flatten().each(function(elt) { + this.deleteFolderElt(elt.readAttribute('id'), true); + }, this); + + this._listFolders({ reload: 1, mboxes: this.folder }); + }, + + subscribeFolder: function(f, sub) + { + var fid = this.getFolderId(f); + DimpCore.doAction('subscribe', { mbox: f, sub: Number(sub) }); + + if (this.showunsub) { + [ $(fid) ].invoke(sub ? 'removeClassName' : 'addClassName', 'unsubFolder'); + } else if (!sub) { + this.deleteFolderElt(fid); + } + }, + + /* Flag actions for message list. */ + _getFlagSelection: function(opts) + { + var vs; + + if (opts.vs) { + vs = opts.vs; + } else if (opts.uid) { + vs = opts.mailbox + ? this.viewport.createSelection('rownum', this.viewport.getAllRows()).search({ imapuid: { equal: [ opts.uid ] }, view: { equal: [ opts.mailbox ] } }) + : this.viewport.createSelection('dataob', opts.uid); + } else { + vs = this.viewport.getSelected(); + } + + return vs; + }, + + _doMsgAction: function(type, opts, args) + { + var vs = this._getFlagSelection(opts); + + if (vs.size()) { + // This needs to be synchronous Ajax if we are calling from a + // popup window because Mozilla will not correctly call the + // callback function if the calling window has been closed. + DimpCore.doAction(type, this.viewport.addRequestParams(args), { uids: vs, ajaxopts: { asynchronous: !(opts.uid && opts.mailbox) } }); + return vs; + } + + return false; + }, + + // spam = (boolean) True for spam, false for innocent + // opts = 'mailbox', 'uid' + reportSpam: function(spam, opts) + { + opts = opts || {}; + if (this._doMsgAction('reportSpam', opts, { spam: Number(spam) })) { + // Indicate to the user that something is happening (since spam + // reporting may not be instantaneous). + this.loadingImg('viewport', true); + } + }, + + // blacklist = (boolean) True for blacklist, false for whitelist + // opts = 'mailbox', 'uid' + blacklist: function(blacklist, opts) + { + opts = opts || {}; + this._doMsgAction('blacklist', opts, { blacklist: blacklist }); + }, + + // opts = 'mailbox', 'uid' + deleteMsg: function(opts) + { + opts = opts || {}; + var vs = this._getFlagSelection(opts); + + // Make sure that any given row is not deleted more than once. Need to + // explicitly mark here because message may already be flagged deleted + // when we load page (i.e. switching to using trash folder). + vs = vs.search({ isdel: { notequal: [ true ] } }); + if (!vs.size()) { + return; + } + vs.set({ isdel: true }); + + opts.vs = vs; + + this._doMsgAction('deleteMessages', opts, {}); + this.updateFlag(vs, '\\deleted', true); + }, + + // flag = (string) IMAP flag name + // set = (boolean) True to set flag + // opts = (Object) 'mailbox', 'noserver', 'uid' + flag: function(flag, set, opts) + { + opts = opts || {}; + var flags = [ (set ? '' : '-') + flag ], + vs = this._getFlagSelection(opts); + + if (!vs.size()) { + return; + } + + switch (flag) { + case '\\answered': + if (set) { + this.updateFlag(vs, '\\flagged', false); + flags.push('-\\flagged'); + } + break; + + case '\\deleted': + vs.set({ isdel: false }); + break; + + case '\\seen': + vs.get('dataob').each(function(s) { + this.updateSeenUID(s, set); + }, this); + break; + } + + this.updateFlag(vs, flag, set); + if (!opts.noserver) { + DimpCore.doAction('flagMessages', this.viewport.addRequestParams({ flags: flags.toJSON(), view: this.folder }), { uids: vs }); + } + }, + + // type = (string) 'seen' or 'unseen' + // mbox = (string) The mailbox to flag + flagAll: function(type, set, mbox) + { + DimpCore.doAction('flagAll', { flags: [ type ].toJSON(), set: Number(set), mbox: mbox }, { callback: this._flagAllCallback.bind(this) }); + }, + + hasFlag: function(f, r) + { + return this.convertFlag(f, r.flag ? r.flag.include(f) : false); + }, + + convertFlag: function(f, set) + { + /* For some flags, we need to do an inverse match (e.g. knowing a + * message is SEEN is not as important as knowing the message lacks + * the SEEN FLAG). This function will determine if, for a given flag, + * the inverse action should be taken on it. */ + return DIMP.conf.flags[f].n ? !set : set; + }, + + updateFlag: function(vs, flag, add) + { + var s = {}; + add = this.convertFlag(flag, add); + + vs.get('dataob').each(function(ob) { + this._updateFlag(ob, flag, add); + + if (this.isSearch()) { + if (s[ob.view]) { + s[ob.view].push(ob.imapuid); + } else { + s[ob.view] = [ ob.imapuid ]; + } + } + }, this); + + /* If this is a search mailbox, also need to update flag in base view, + * if it is in the buffer. */ + $H(s).each(function(m) { + var tmp = this.viewport.getSelection(m.key).search({ imapuid: { equal: m.value }, view: { equal: m.key } }); + if (tmp.size()) { + this._updateFlag(tmp.get('dataob').first(), flag, add); + } + }, this); + }, + + _updateFlag: function(ob, flag, add) + { + ob.flag = ob.flag + ? ob.flag.without(flag) + : []; + + if (add) { + ob.flag.push(flag); + } + + this.viewport.updateRow(ob); + }, + + /* Miscellaneous folder actions. */ + purgeDeleted: function() + { + DimpCore.doAction('purgeDeleted', this.viewport.addRequestParams({})); + }, + + modifyPoll: function(folder, add) + { + DimpCore.doAction('modifyPoll', { add: Number(add), mbox: folder }, { callback: this._modifyPollCallback.bind(this) }); + }, + + _modifyPollCallback: function(r) + { + r = r.response; + var f = r.mbox, fid, p = { response: { poll: {} } }; + fid = $(this.getFolderId(f)); + + if (r.add) { + p.response.poll[f] = r.poll.u; + fid.store('u', 0); + } else { + p.response.poll[f] = 0; + } + + if (!r.add) { + fid.store('u', null); + this.updateUnseenStatus(f, 0); + } + }, + + loadingImg: function(id, show) + { + DimpCore.loadingImg(id + 'Loading', id == 'viewport' ? 'msgSplitPane' : 'previewPane', show); + }, + + // p = (element) Parent element + // c = (element) Child element + isSubfolder: function(p, c) + { + var sf = $(this.getSubFolderId(p.identify())); + return sf && c.descendantOf(sf); + }, + + /* Pref updating function. */ + _updatePrefs: function(pref, value) + { + new Ajax.Request(DimpCore.addURLParam(DIMP.conf.URI_PREFS), { parameters: { pref: pref, value: value } }); + }, + + /* Onload function. */ + onDomLoad: function() + { + DimpCore.init(); + + var DM = DimpCore.DMenu, tmp; + + /* Register global handlers now. */ + document.observe('keydown', this.keydownHandler.bindAsEventListener(this)); + document.observe('change', this.changeHandler.bindAsEventListener(this)); + document.observe('dblclick', this.dblclickHandler.bindAsEventListener(this)); + Event.observe(window, 'resize', this.onResize.bind(this)); + + /* Limit to folders sidebar only. */ + $('foldersSidebar').observe('mouseover', this.mouseoverHandler.bindAsEventListener(this)); + + /* Show page now. */ + $('sidebar').setStyle({ width: DIMP.conf.sidebar_width }); + $('dimpLoading').hide(); + $('dimpPage').show(); + + /* Create splitbar for sidebar. */ + this.splitbar = new Element('DIV', { className: 'splitBarVertSidebar' }).setStyle({ height: document.viewport.getHeight() + 'px', left: $('sidebar').clientWidth + 'px' }); + $('sidebar').insert({ after: this.splitbar }); + new Drag(this.splitbar, { + constraint: 'horizontal', + ghosting: true, + nodrop: true + }); + + $('dimpmain').setStyle({ left: ($('sidebar').clientWidth + this.splitbar.clientWidth) + 'px' }); + + /* Init quicksearch. These needs to occur before loading the message + * list since it may be disabled if we are in a search mailbox. */ + if ($('qsearch')) { + $('qsearch_input').observe('blur', this._quicksearchOnBlur.bind(this)); + DimpCore.addContextMenu({ + id: 'qsearch_icon', + left: true, + offset: 'qsearch', + type: 'qsearchopts' + }); + DimpCore.addContextMenu({ + id: 'qsearch_icon', + left: false, + offset: 'qsearch', + type: 'qsearchopts' + }); + DM.addSubMenu('ctx_qsearchopts_by', 'ctx_qsearchby'); + DM.addSubMenu('ctx_qsearchopts_filter', 'ctx_flag'); + DM.addSubMenu('ctx_qsearchopts_filternot', 'ctx_flag'); + } + + /* Store these text strings for updating purposes. */ + DIMP.text.getmail = $('checkmaillink').down('A').innerHTML; + DIMP.text.refresh = $('refreshlink').down('A').innerHTML; + DIMP.text.showalog = $('alertsloglink').down('A').innerHTML; + + /* Initialize the starting page. */ + tmp = location.hash; + if (!tmp.empty() && tmp.startsWith('#')) { + tmp = (tmp.length == 1) ? "" : tmp.substring(1); + } + + if (!tmp.empty()) { + this.go(decodeURIComponent(tmp)); + } else if (DIMP.conf.login_view == 'inbox') { + this.go('folder:INBOX'); + } else { + this.go('portal'); + this.loadMailbox('INBOX', { background: true }); + } + + /* Create the folder list. Any pending notifications will be caught + * via the return from this call. */ + this._listFolders({ initial: 1, mboxes: this.folder} ); + + this._setQsearchText(true); + + /* Add popdown menus. Check for disabled compose at the same time. */ + DimpCore.addPopdown('button_other', 'otheractions', true); + DimpCore.addPopdown('folderopts_link', 'folderopts', true); + + DM.addSubMenu('ctx_message_reply', 'ctx_reply'); + DM.addSubMenu('ctx_message_forward', 'ctx_forward'); + [ 'ctx_message_', 'oa_' ].each(function(i) { + if ($(i + 'setflag')) { + DM.addSubMenu(i + 'setflag', 'ctx_flag'); + DM.addSubMenu(i + 'unsetflag', 'ctx_flag'); + } + }); + DM.addSubMenu('ctx_folder_setflag', 'ctx_folder_flag'); + + if (DIMP.conf.disable_compose) { + $('button_reply', 'button_forward').compact().invoke('up', 'SPAN').concat($('button_compose', 'composelink', 'ctx_contacts_new')).compact().invoke('remove'); + } else { + DimpCore.addPopdown('button_reply', 'reply', false, true); + DimpCore.addPopdown('button_forward', 'forward', false, true); + } + + DimpCore.addContextMenu({ + id: 'msglistHeader', + type: 'mboxsort' + }); + + new Drop('dropbase', this._folderDropConfig); + + if (DIMP.conf.toggle_pref) { + this._toggleHeaders($('th_expand')); + } + + /* Remove unavailable menu items. */ + if (!$('GrowlerLog')) { + $('alertsloglink').remove(); + } + + /* Check for new mail. */ + this.setPoll(); + }, + + /* Resize function. */ + onResize: function() + { + if (this.resize) { + clearTimeout(this.resize); + } + + this.resize = this._onResize.bind(this).delay(0.1); + }, + + _onResize: function() + { + this._sizeFolderlist(); + this.splitbar.setStyle({ height: document.viewport.getHeight() + 'px' }); + }, + + /* Extend AJAX exception handling. */ + onAjaxException: function(parentfunc, r, e) + { + /* Make sure loading images are closed. */ + this.loadingImg('msg', false); + this.loadingImg('viewport', false); + DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_error } ]); + parentfunc(r, e); + } + +}; + +/* Need to add after DimpBase is defined. */ +DimpBase._msgDragConfig = { + classname: 'msgdrag', + scroll: 'normalfolders', + threshold: 5, + caption: DimpBase.dragCaption.bind(DimpBase) +}; + +DimpBase._folderDragConfig = { + classname: 'folderdrag', + ghosting: true, + offset: { x: 15, y: 0 }, + scroll: 'normalfolders', + threshold: 5 +}; + +DimpBase._folderDropConfig = { + caption: function(drop, drag, e) { + var m, + d = drag.retrieve('l'), + ftype = drop.retrieve('ftype'), + l = drop.retrieve('l'); + + if (drop == $('dropbase')) { + return DIMP.text.moveto.sub('%s', d).sub('%s', DIMP.text.baselevel); + } + + switch (e.type) { + case 'mousemove': + m = (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto; + break; + + case 'keydown': + /* Can't use ctrlKey here since different browsers handle the + * ctrlKey in different ways when it comes to firing keyboard + * events. */ + m = (e.keyCode == 17) ? DIMP.text.copyto : DIMP.text.moveto; + break; + + case 'keyup': + m = (e.keyCode == 17) + ? DIMP.text.moveto + : (e.ctrlKey) ? DIMP.text.copyto : DIMP.text.moveto; + break; + } + + if (drag.hasClassName('folder')) { + return (ftype != 'special' && !DimpBase.isSubfolder(drag, drop)) ? m.sub('%s', d).sub('%s', l) : ''; + } + + return ftype != 'container' ? m.sub('%s', DimpBase.dragCaption()).sub('%s', l) : ''; + }, + keypress: true +}; + +/* Drag/drop listeners. */ +document.observe('DragDrop2:drag', DimpBase.onDrag.bindAsEventListener(DimpBase)); +document.observe('DragDrop2:drop', DimpBase.folderDropHandler.bindAsEventListener(DimpBase)); +document.observe('DragDrop2:end', DimpBase.onDragEnd.bindAsEventListener(DimpBase)); +document.observe('DragDrop2:mousedown', DimpBase.onDragMouseDown.bindAsEventListener(DimpBase)); +document.observe('DragDrop2:mouseup', DimpBase.onDragMouseUp.bindAsEventListener(DimpBase)); + +/* Route AJAX responses through ViewPort. */ +DimpCore.onDoActionComplete = function(r) { + DimpBase.deleteCallback(r); + if (DimpBase.viewport) { + DimpBase.viewport.parseJSONResponse(r); + } + DimpBase.pollCallback(r); +}; + +/* Click handler. */ +DimpCore.clickHandler = DimpCore.clickHandler.wrap(DimpBase.clickHandler.bind(DimpBase)); + +/* ContextSensitive handlers. */ +DimpCore.contextOnClick = DimpCore.contextOnClick.wrap(DimpBase.contextOnClick.bind(DimpBase)); +DimpCore.contextOnShow = DimpCore.contextOnShow.wrap(DimpBase.contextOnShow.bind(DimpBase)); + +/* Extend AJAX exception handling. */ +DimpCore.doActionOpts.onException = DimpCore.doActionOpts.onException.wrap(DimpBase.onAjaxException.bind(DimpBase)); + +/* Initialize onload handler. */ +document.observe('dom:loaded', DimpBase.onDomLoad.bind(DimpBase)); diff --git a/imp/js/dimpcore.js b/imp/js/dimpcore.js new file mode 100644 index 000000000..369aa0e98 --- /dev/null +++ b/imp/js/dimpcore.js @@ -0,0 +1,595 @@ +/** + * dimpcore.js - Dimp UI application logic. + * + * Copyright 2005-2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + */ + +/* DimpCore object. */ +var DimpCore = { + // Vars used and defaulting to null/false: + // DMenu, Growler, inAjaxCallback, is_init, is_logout + // onDoActionComplete + alarms: {}, + growler_log: true, + server_error: 0, + + doActionOpts: { + onException: function(r, e) { DimpCore.debug('onException', e); }, + onFailure: function(t, o) { DimpCore.debug('onFailure', t); }, + evalJS: false, + evalJSON: true + }, + + debug: function(label, e) + { + if (!this.is_logout && window.console && window.console.error) { + window.console.error(label, Prototype.Browser.Gecko ? e : $H(e).inspect()); + } + }, + + // Convert object to an IMP UID Range string. See IMP::toRangeString() + // ob = (object) mailbox name as keys, values are array of uids. + toRangeString: function(ob) + { + var str = ''; + + $H(ob).each(function(o) { + if (!o.value.size()) { + return; + } + + var u = o.value.numericSort(), + first = u.shift(), + last = first, + out = []; + + u.each(function(k) { + if (last + 1 == k) { + last = k; + } else { + out.push(first + (last == first ? '' : (':' + last))); + first = last = k; + } + }); + out.push(first + (last == first ? '' : (':' + last))); + str += '{' + o.key.length + '}' + o.key + out.join(','); + }); + + return str; + }, + + // Parses an IMP UID Range string. See IMP::parseRangeString() + // str = (string) An IMP UID range string. + parseRangeString: function(str) + { + var count, end, i, mbox, uidstr, + mlist = {}, + uids = []; + str = str.strip(); + + while (!str.blank()) { + if (!str.startsWith('{')) { + break; + } + i = str.indexOf('}'); + count = Number(str.substr(1, i - 1)); + mbox = str.substr(i + 1, count); + i += count + 1; + end = str.indexOf('{', i); + if (end == -1) { + uidstr = str.substr(i); + str = ''; + } else { + uidstr = str.substr(i, end - i); + str = str.substr(end); + } + + uidstr.split(',').each(function(e) { + var r = e.split(':'); + if (r.size() == 1) { + uids.push(Number(e)); + } else { + uids = uids.concat($A($R(Number(r[0]), Number(r[1])))); + } + }); + + mlist[mbox] = uids; + } + + return mlist; + }, + + // 'opts' -> ajaxopts, callback, uids + doAction: function(action, params, opts) + { + params = $H(params); + opts = opts || {}; + + var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {}); + + if (opts.uids) { + if (opts.uids.viewport_selection) { + opts.uids = this.selectionToRange(opts.uids); + } + params.set('uid', this.toRangeString(opts.uids)); + } + + ajaxopts.parameters = this.addRequestParams(params); + ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this); + + new Ajax.Request(DIMP.conf.URI_AJAX + action, ajaxopts); + }, + + // 'opts' -> ajaxopts, callback + submitForm: function(form, opts) + { + opts = opts || {}; + var ajaxopts = Object.extend(Object.clone(this.doActionOpts), opts.ajaxopts || {}); + ajaxopts.onComplete = function(t, o) { this.doActionComplete(t, opts.callback); }.bind(this); + $(form).request(ajaxopts); + }, + + selectionToRange: function(s) + { + var b = s.getBuffer(), + tmp = {}; + + if (b.getMetaData('search')) { + s.get('uid').each(function(r) { + var parts = r.split(DIMP.conf.IDX_SEP); + if (tmp[parts[0]]) { + tmp[parts[0]].push(parts[1]); + } else { + tmp[parts[0]] = [ parts[1] ]; + } + }); + } else { + tmp[b.getView()] = s.get('uid'); + } + + return tmp; + }, + + // params - (Hash) + addRequestParams: function(params) + { + var p = params.clone(); + + if (DIMP.conf.SESSION_ID) { + p.update(DIMP.conf.SESSION_ID.toQueryParams()); + } + + return p; + }, + + doActionComplete: function(request, callback) + { + this.inAjaxCallback = true; + + if (!request.responseJSON) { + if (++this.server_error == 3) { + this.showNotifications([ { type: 'horde.error', message: DIMP.text.ajax_timeout } ]); + } + this.inAjaxCallback = false; + return; + } + + var r = request.responseJSON; + + if (!r.msgs) { + r.msgs = []; + } + + if (r.response && Object.isFunction(callback)) { + try { + callback(r); + } catch (e) { + this.debug('doActionComplete', e); + } + } + + if (this.server_error >= 3) { + r.msgs.push({ type: 'horde.success', message: DIMP.text.ajax_recover }); + } + this.server_error = 0; + + this.showNotifications(r.msgs); + + if (r.response && this.onDoActionComplete) { + this.onDoActionComplete(r.response); + } + + this.inAjaxCallback = false; + }, + + setTitle: function(title) + { + document.title = DIMP.conf.name + ' :: ' + title; + }, + + showNotifications: function(msgs) + { + if (!msgs.size() || this.is_logout) { + return; + } + + msgs.find(function(m) { + switch (m.type) { + case 'horde.ajaxtimeout': + this.logout(m.message); + return true; + + case 'horde.alarm': + if (!this.alarms[m.flags.alarm.id]) { + this.Growler.growl(m.flags.alarm.title + ': ' + m.flags.alarm.text, { + className: 'horde-alarm', + sticky: 1, + log: 1 + }); + this.alarms[m.flags.alarm.id] = 1; + } + break; + + case 'horde.error': + case 'horde.message': + case 'horde.success': + case 'horde.warning': + this.Growler.growl(m.message, { + className: m.type.replace('.', '-'), + life: (m.type == 'horde.error' ? 12 : 8), + log: 1 + }); + break; + + case 'imp.reply': + case 'imp.forward': + case 'imp.redirect': + this.Growler.growl(m.message, { + className: m.type.replace('.', '-'), + life: 8 + }); + break; + } + }, this); + }, + + compose: function(type, args) + { + var url = DIMP.conf.URI_COMPOSE; + args = args || {}; + if (type) { + args.type = type; + } + this.popupWindow(this.addURLParam(url, args), 'compose' + new Date().getTime()); + }, + + popupWindow: function(url, name, onload) + { + var opts = { + height: DIMP.conf.popup_height, + name: name.gsub(/\W/, '_'), + noalert: true, + onload: onload, + url: url, + width: DIMP.conf.popup_width + }; + + if (!Horde.popup(opts)) { + this.showNotifications([ { type: 'horde.warning', message: DIMP.text.popup_block } ]); + } + }, + + closePopup: function() + { + // Mozilla bug/feature: it will not close a browser window + // automatically if there is code remaining to be performed (or, at + // least, not here) unless the mouse is moved or a keyboard event + // is triggered after the callback is complete. (As of FF 2.0.0.3 and + // 1.5.0.11). So wait for the callback to complete before attempting + // to close the window. + if (this.inAjaxCallback) { + this.closePopup.bind(this).defer(); + } else { + window.close(); + } + }, + + logout: function(url) + { + this.is_logout = true; + this.redirect(url || (DIMP.conf.URI_AJAX + 'logOut')); + }, + + redirect: function(url, force) + { + var ptr = parent.frames.horde_main ? parent : window; + + ptr.location.assign(this.addURLParam(url)); + + // Catch browsers that don't redirect on assign(). + if (force && !Prototype.Browser.WebKit) { + (function() { ptr.location.reload(); }).delay(0.5); + } + }, + + loadingImg: function(elt, id, show) + { + elt = $(elt); + + if (show) { + elt.clonePosition(id, { setHeight: false, setLeft: false, setWidth: false }).show(); + } else { + elt.fade({ duration: 0.2 }); + } + }, + + toggleButtons: function(elts, disable) + { + elts.each(function(b) { + var tmp; + [ b.up() ].invoke(disable ? 'addClassName' : 'removeClassName', 'disabled'); + if (this.DMenu && + (tmp = b.next('.popdown'))) { + this.DMenu.disable(tmp.identify(), true, disable); + } + }, this); + }, + + // p = (Element) Parent element + // t = (string) Context menu type + // trigger = (boolean) Trigger popdown on button click? + // d = (boolean) Disabled? + addPopdown: function(p, t, trigger, d) + { + var elt = new Element('SPAN', { className: 'iconImg popdownImg popdown' }); + p = $(p); + + p.insert({ after: elt }); + + if (trigger) { + this.addContextMenu({ + disable: d, + id: p.identify(), + left: true, + offset: p.up(), + type: t + }); + } + + this.addContextMenu({ + disable: d, + id: elt.identify(), + left: true, + offset: elt.up(), + type: t + }); + + return elt; + }, + + addContextMenu: function(p) + { + if (this.DMenu) { + this.DMenu.addElement(p.id, 'ctx_' + p.type, p); + } + }, + + /* Add dropdown menus to addresses. */ + buildAddressLinks: function(alist, elt) + { + var base, tmp, + cnt = alist.size(); + + if (cnt > 15) { + tmp = $('largeaddrspan').cloneNode(true).writeAttribute('id', 'largeaddrspan_active'); + elt.insert(tmp); + base = tmp.down('.dispaddrlist'); + tmp = tmp.down('.largeaddrlist'); + tmp.setText(tmp.getText().replace('%d', cnt)); + } else { + base = elt; + } + + alist.each(function(o, i) { + var a; + if (o.raw) { + a = o.raw; + } else { + a = new Element('A', { className: 'address' }).store({ personal: o.personal, email: o.inner, address: (o.personal ? (o.personal + ' <' + o.inner + '>') : o.inner) }); + if (o.personal) { + a.writeAttribute({ title: o.inner }).insert(o.personal.escapeHTML()); + } else { + a.insert(o.inner.escapeHTML()); + } + this.DMenu.addElement(a.identify(), 'ctx_contacts', { offset: a, left: true }); + } + base.insert(a); + if (i + 1 != cnt) { + base.insert(', '); + } + }, this); + + return elt; + }, + + /* Add message log info to message view. */ + updateMsgLog: function(log) + { + var tmp = ''; + log.each(function(entry) { + tmp += '
    • ' + entry.m + '
    • '; + }); + $('msgloglist').down('UL').update(tmp); + }, + + /* Removes event handlers from address links. */ + removeAddressLinks: function(id) + { + id.select('.address').each(function(elt) { + this.DMenu.removeElement(elt.identify()); + }, this); + }, + + addURLParam: function(url, params) + { + var q = url.indexOf('?'); + params = $H(params); + + if (DIMP.conf.SESSION_ID) { + params.update(DIMP.conf.SESSION_ID.toQueryParams()); + } + + if (q != -1) { + params.update(url.toQueryParams()); + url = url.substring(0, q); + } + + return params.size() ? (url + '?' + params.toQueryString()) : url; + }, + + reloadMessage: function(params) + { + if (typeof DimpFullmessage != 'undefined') { + window.location = this.addURLParam(document.location.href, params); + } else { + DimpBase.loadPreview(null, params); + } + }, + + /* Mouse click handler. */ + clickHandler: function(e) + { + if (e.isRightClick()) { + return; + } + + var elt = e.element(), id, tmp; + + while (Object.isElement(elt)) { + id = elt.readAttribute('id'); + + switch (id) { + case 'largeaddrspan_active': + tmp = elt.down(); + if (!tmp.next().visible() || + e.element().hasClassName('largeaddrlist')) { + [ tmp.down(), tmp.down(1), tmp.next() ].invoke('toggle'); + } + break; + + default: + // CSS class based matching + if (elt.hasClassName('unblockImageLink')) { + IMP.unblockImages(e); + } else if (elt.hasClassName('toggleQuoteShow')) { + [ elt, elt.next() ].invoke('toggle'); + elt.next(1).blindDown({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } }); + } else if (elt.hasClassName('toggleQuoteHide')) { + [ elt, elt.previous() ].invoke('toggle'); + elt.next().blindUp({ duration: 0.2, queue: { position: 'end', scope: 'showquote', limit: 2 } }); + } else if (elt.hasClassName('pgpVerifyMsg')) { + elt.replace(DIMP.text.verify); + DimpCore.reloadMessage({ pgp_verify_msg: 1 }); + e.stop(); + } else if (elt.hasClassName('smimeVerifyMsg')) { + elt.replace(DIMP.text.verify); + DimpCore.reloadMessage({ smime_verify_msg: 1 }); + e.stop(); + } + break; + } + + elt = elt.up(); + } + }, + + contextOnShow: function(e) + { + var tmp; + + switch (e.memo) { + case 'ctx_contacts': + tmp = $(e.memo).down('DIV.contactAddr'); + if (tmp) { + tmp.next().remove(); + tmp.remove(); + } + + // Add e-mail info to context menu if personal name is shown on + // page. + if (e.element().retrieve('personal')) { + $(e.memo) + .insert({ top: new Element('DIV', { className: 'sep' }) }) + .insert({ top: new Element('DIV', { className: 'contactAddr' }).insert(e.element().retrieve('email').escapeHTML()) }); + } + break; + } + }, + + contextOnClick: function(e) + { + var baseelt = e.element(); + + switch (e.memo.elt.readAttribute('id')) { + case 'ctx_contacts_new': + this.compose('new', { to: baseelt.retrieve('address') }); + break; + + case 'ctx_contacts_add': + this.doAction('addContact', { name: baseelt.retrieve('personal'), email: baseelt.retrieve('email') }, {}, true); + break; + } + }, + + /* DIMP initialization function. */ + init: function() + { + if (this.is_init) { + return; + } + this.is_init = true; + + if (typeof ContextSensitive != 'undefined') { + this.DMenu = new ContextSensitive(); + document.observe('ContextSensitive:click', this.contextOnClick.bindAsEventListener(this)); + document.observe('ContextSensitive:show', this.contextOnShow.bindAsEventListener(this)); + } + + /* Add Growler notification handler. */ + this.Growler = new Growler({ + location: 'br', + log: this.growler_log, + noalerts: DIMP.text.noalerts + }); + + /* Add click handler. */ + document.observe('click', DimpCore.clickHandler.bindAsEventListener(DimpCore)); + + /* Catch dialog actions. */ + document.observe('IMPDialog:success', function(e) { + switch (e.memo) { + case 'pgpPersonal': + case 'pgpSymmetric': + case 'smimePersonal': + IMPDialog.noreload = true; + this.reloadMessage({}); + break; + } + }.bindAsEventListener(this)); + + /* Determine base window. Need a try/catch block here since, if the + * page was loaded by an opener out of this current domain, this will + * throw an exception. */ + try { + if (parent.opener && + parent.opener.location.host == window.location.host && + parent.opener.DimpCore) { + DIMP.baseWindow = parent.opener.DIMP.baseWindow || parent.opener; + } + } catch (e) {} + } + +}; diff --git a/imp/js/viewport.js b/imp/js/viewport.js new file mode 100644 index 000000000..7fb8cde04 --- /dev/null +++ b/imp/js/viewport.js @@ -0,0 +1,1846 @@ +/** + * viewport.js - Code to create a viewport window, with optional split pane + * functionality. + * + * Usage: + * ====== + * var viewport = new ViewPort({ options }); + * + * Required options: + * ----------------- + * ajax_url: (string) The URL to send the viewport requests to. + * This URL should return its response in an object named + * 'ViewPort' (other information can be returned in the response and + * will be ignored by this class). + * container: (Element/string) A DOM element/ID of the container that holds + * the viewport. This element should be empty and have no children. + * onContent: (function) A function that takes 2 arguments - the data object + * for the row and a string indicating the current pane_mode. + * + * This function MUST return the HTML representation of the row. + * + * This representation MUST include both the DOM ID (stored in + * the VP_domid data entry) and the CSS class name (stored as an + * array in the VP_bg data entry) in the outermost element. + * + * Selected rows will contain the classname 'vpRowSelected'. + * + * + * Optional options: + * ----------------- + * ajax_opts: (object) Any additional options to pass to the Ajax.Request + * object when sending an AJAX message. + * buffer_pages: (integer) The number of viewable pages to send to the browser + * per server access when listing rows. + * empty_msg: (string) A string to display when the view is empty. Inserted in + * a SPAN element with class 'vpEmpty'. + * limit_factor: (integer) When browsing through a list, if a user comes + * within this percentage of the end of the current cached + * viewport, send a background request to the server to retrieve + * the next slice. + * list_class: (string) The CSS class to use for the list container. + * lookbehind: (integer) What percentage of the received buffer should be + * used to download rows before the given row number? + * onAjaxFailure: (function) Callback function that handles a failure response + * from an AJAX request. + * params: (XMLHttpRequest object) + * (mixed) Result of evaluating the X-JSON response + * header, if any (can be null). + * return: NONE + * onAjaxRequest: (function) Callback function that allows additional + * parameters to be added to the outgoing AJAX request. + params: (string) The current view. + return: (Hash) Parameters to add to the outgoing request. + * onAjaxResponse: (function) Callback function that allows user-defined code + * to additionally process the AJAX return data. + * params: (XMLHttpRequest object) + * (mixed) Result of evaluating the X-JSON response + * header, if any (can be null). + * return: NONE + * onCachedList: (function) Callback function that allows the cache ID string + * to be dynamically generated. + params: (string) The current view. + return: (string) The cache ID string to use. + * onContentOffset: (function) Callback function that alters the starting + * offset of the content about to be rendered. + * params: (integer) The current offset. + * return: (integer) The altered offset. + * onSlide: (function) Callback function that is triggered when the + * viewport slider bar is moved. + * params: NONE + * return: NONE + * page_size: (integer) Default page size to view on load. Only used if + * pane_mode is 'horiz'. + * pane_data: (Element/string) A DOM element/ID of the container to hold + * the split pane data. This element will be moved inside of the + * container element. + * pane_mode: (string) The split pane mode to show on load? Either empty, + * 'horiz', or 'vert'. + * pane_width: (integer) The default pane width to use on load. Only used if + * pane_mode is 'vert'. + * split_bar_class: (object) The CSS class(es) to use for the split bar. + * Takes two properties: 'horiz' and 'vert'. + * wait: (integer) How long, in seconds, to wait before displaying an + * informational message to users that the list is still being + * built. + * + * + * Custom events: + * -------------- + * Custom events are triggered on the container element. The parameters given + * below are available through the 'memo' property of the Event object. + * + * ViewPort:add + * Fired when a row has been added to the screen. + * params: (Element) The viewport row being added. + * + * ViewPort:cacheUpdate + * Fired when the internal cached data of a view is changed. + * params: (string) View which is being updated. + * + * ViewPort:clear + * Fired when a row is being removed from the screen. + * params: (Element) The viewport row being removed. + * + * ViewPort:contentComplete + * Fired when the view has changed and all viewport rows have been added. + * params: NONE + * + * ViewPort:deselect + * Fired when rows are deselected. + * params: (object) opts = (object) Boolean options [right] + * vs = (ViewPort_Selection) A ViewPort_Selection object. + * + * ViewPort:endFetch + * Fired when a fetch AJAX response is completed. + * params: (string) Current view. + * + * ViewPort:fetch + * Fired when a non-background AJAX response is sent. + * params: (string) Current view. + * + * ViewPort:select + * Fired when rows are selected. + * params: (object) opts = (object) Boolean options [delay, right] + * vs = (ViewPort_Selection) A ViewPort_Selection object. + * + * ViewPort:splitBarChange + * Fired when the splitbar is moved. + * params: (string) The current pane mode ('horiz' or 'vert'). + * + * ViewPort:splitBarEnd + * Fired when the splitbar is released. + * params: (string) The current pane mode ('horiz' or 'vert'). + * + * ViewPort:splitBarStart + * Fired when the splitbar is initially clicked. + * params: (string) The current pane mode ('horiz' or 'vert'). + * + * ViewPort:wait + * Fired if viewport_wait seconds have passed since request was sent. + * params: (string) Current view. + * + * + * Outgoing AJAX request has the following params: + * ----------------------------------------------- + * For ALL requests: + * cache: (string) The list of uids cached on the browser. + * cacheid: (string) A unique string that changes whenever the viewport + * list changes. + * initial: (integer) This is the initial browser request for this view. + * requestid: (integer) A unique identifier for this AJAX request. + * view: (string) The view of the request. + * + * For a row request: + * slice: (string) The list of rows to retrieve from the server. + * In the format: [first_row]:[last_row] + * + * For a search request: + * after: (integer) The number of rows to return after the selected row. + * before: (integer) The number of rows to return before the selected row. + * search: (JSON object) The search query. + * + * For a rangeslice request: + * rangeslice: (integer) If present, indicates that slice is a rangeslice + * request. + * slice: (string) The list of rows to retrieve from the server. + * In the format: [first_row]:[last_row] + * + * + * Incoming AJAX response has the following params: + * ------------------------------------------------ + * cacheid: (string) A unique string that changes whenever the viewport + * list changes. + * data: (object) Data for each entry that is passed to the template to create + * the viewable rows. Keys are a unique ID (see also the 'rowlist' + * entry). Values are the data objects. Internal keys for these data + * objects must NOT begin with the string 'VP_'. + * disappear: (array) If update is set, this is the list of unique IDs that + * have been cached by the browser but no longer appear on the + * server. + * label: (string) [REQUIRED when initial is true] The label to use for the + * view. + * metadata [optional]: (object) Metadata for the view. Entries in buffer are + * updated with these entries (unless resetmd is set). + * rangelist [optional]: (object) The list of unique IDs -> rownumbers that + * correspond the the given request. Only returned for + * a rangeslice request. + * requestid: (string) The request ID sent in the outgoing AJAX request. + * reset [optional]: (integer) If set, purges all cached data. + * resetmd [optional]: (integer) If set, purges all user metadata. + * rowlist: (object) A mapping of unique IDs (keys) to the row numbers + * (values). Row numbers start at 1. + * rownum [optional]: (integer) The row number to position screen on. + * totalrows: (integer) Total number of rows in the view. + * update [optional]: (integer) If set, update the rowlist instead of + * overwriting it. + * updatecacheid [optional]: (string) If set, simply update the cacheid with + * the new value. Indicates that the browser + * contains the up-to-date version of the cache. + * view: (string) The view ID of the request. + * + * + * Data entries: + * ------------- + * In addition to the data provided from the server, the following + * dynamically created entries are also available: + * VP_domid: (string) The DOM ID of the row. + * VP_id: (string) The unique ID used to store the data entry. + * VP_rownum: (integer) The row number of the row. + * + * + * Scroll bars use ars styled using these CSS class names: + * ------------------------------------------------------- + * vpScroll - The scroll bar container. + * vpScrollUp - The UP arrow. + * vpScrollCursor - The cursor used to slide within the bounds. + * vpScrollDown - The DOWN arrow. + * + * + * Requires prototypejs 1.6+, scriptaculous 1.8+ (effects.js only), and + * Horde's dragdrop2.js and slider2.js. + * + * Copyright 2005-2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + */ + +/** + * ViewPort + */ +var ViewPort = Class.create({ + + initialize: function(opts) + { + this.opts = Object.extend({ + buffer_pages: 10, + limit_factor: 35, + lookbehind: 40, + split_bar_class: {} + }, opts); + + this.opts.container = $(opts.container); + this.opts.pane_data = $(opts.pane_data); + + this.opts.content = new Element('DIV', { className: opts.list_class }).setStyle({ float: 'left', overflow: 'hidden' }); + this.opts.container.insert(this.opts.content); + + this.scroller = new ViewPort_Scroller(this); + + this.split_pane = { + curr: null, + currbar: null, + horiz: { + loc: opts.page_size + }, + init: false, + spacer: null, + vert: { + width: opts.pane_width + } + }; + this.views = {}; + + this.pane_mode = opts.pane_mode; + + this.isbusy = this.page_size = null; + this.request_num = 1; + + // Init empty string now. + this.empty_msg = new Element('SPAN', { className: 'vpEmpty' }).insert(opts.empty_msg); + + // Set up AJAX response function. + this.ajax_response = this.opts.onAjaxResponse || this._ajaxRequestComplete.bind(this); + + Event.observe(window, 'resize', this.onResize.bind(this)); + }, + + // view = (string) ID of view. + // opts = (object) background: (boolean) Load view in background? + // search: (object) Search parameters + loadView: function(view, opts) + { + var buffer, curr, ps, + f_opts = {}, + init = true; + + this._clearWait(); + + // Need a page size before we can continue - this is what determines + // the slice size to request from the server. + if (this.page_size === null) { + ps = this.getPageSize(this.pane_mode ? 'default' : 'max'); + if (isNaN(ps)) { + return this.loadView.bind(this, view, opts).defer(); + } + this.page_size = ps; + } + + if (this.view) { + if (!opts.background && (view != this.view)) { + // Need to store current buffer to save current offset + buffer = this._getBuffer(); + buffer.setMetaData({ offset: this.currentOffset() }, true); + this.views[this.view] = buffer; + } + init = false; + } + + if (opts.background) { + f_opts = { background: true, view: view }; + } else { + if (!this.view) { + this.onResize(true); + } else if (this.view != view) { + this.active_req = null; + } + this.view = view; + } + + if (curr = this.views[view]) { + this._updateContent(curr.getMetaData('offset') || 0, f_opts); + if (!opts.background) { + this._ajaxRequest({ checkcache: 1 }); + } + return; + } + + if (!init) { + this.visibleRows().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); + this.opts.content.update(); + this.scroller.clear(); + } + + this.views[view] = buffer = this._getBuffer(view, true); + + if (opts.search) { + f_opts.search = opts.search; + } else { + f_opts.offset = 0; + } + + f_opts.initial = 1; + + this._fetchBuffer(f_opts); + }, + + // view = ID of view + deleteView: function(view) + { + delete this.views[view]; + }, + + // rownum = (integer) Row number + // opts = (Object) [noupdate, top] TODO + scrollTo: function(rownum, opts) + { + var s = this.scroller; + opts = opts || {}; + + s.noupdate = opts.noupdate; + + switch (this.isVisible(rownum)) { + case -1: + s.moveScroll(rownum - 1); + break; + + case 0: + if (opts.top) { + s.moveScroll(rownum - 1); + } + break; + + case 1: + s.moveScroll(Math.min(rownum - 1, this.getMetaData('total_rows') - this.getPageSize())); + break; + } + + s.noupdate = false; + }, + + // rownum = (integer) Row number + isVisible: function(rownum) + { + var offset = this.currentOffset(); + return (rownum < offset + 1) + ? -1 + : ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0); + }, + + // params = (object) Parameters to add to outgoing URL + reload: function(params) + { + this._fetchBuffer({ + offset: this.currentOffset(), + params: $H(params), + purge: true + }); + }, + + // vs = (Viewport_Selection) A Viewport_Selection object. + // opts = (object) TODO [noupdate, view] + remove: function(vs, opts) + { + if (!vs.size()) { + return; + } + + if (this.isbusy) { + this.remove.bind(this, vs, opts).defer(); + return; + } + + this.isbusy = true; + opts = opts || {}; + + var args = { duration: 0.2, to: 0.01 }, + visible = vs.get('div'); + + this.deselect(vs); + + // If we have visible elements to remove, only call refresh after + // the last effect has finished. + if (visible.size()) { + // Set 'to' to a value slightly above 0 to prevent fade() + // from auto hiding. Hiding is unnecessary, since we will be + // removing from the document shortly. + visible.slice(0, -1).invoke('fade', args); + args.afterFinish = this._removeids.bind(this, vs, opts); + visible.last().fade(args); + } else { + this._removeids(vs, opts); + } + }, + + // vs = (Viewport_Selection) A Viewport_Selection object. + // opts = (object) TODO [noupdate, view] + _removeids: function(vs, opts) + { + this._getBuffer(opts.view).setMetaData({ total_rows: this.getMetaData('total_rows', opts.view) - vs.size() }, true); + + this._getBuffer().remove(vs.get('rownum')); + this.opts.container.fire('ViewPort:cacheUpdate', opts.view || this.view); + + if (!opts.noupdate) { + this.requestContentRefresh(this.currentOffset()); + } + + this.isbusy = false; + }, + + // nowait = (boolean) If true, don't delay before resizing. + // size = (integer) The page size to use instead of auto-determining. + onResize: function(nowait, size) + { + if (!this.opts.content.visible()) { + return; + } + + if (this.resizefunc) { + clearTimeout(this.resizefunc); + } + + if (nowait) { + this._onResize(size); + } else { + this.resizefunc = this._onResize.bind(this, size).delay(0.1); + } + }, + + // size = (integer) The page size to use instead of auto-determining. + _onResize: function(size) + { + var h, + c = this.opts.content, + c_opts = {}, + lh = this._getLineHeight(), + sp = this.split_pane; + + if (size) { + this.page_size = size; + } + + if (this.view && sp.curr != this.pane_mode) { + c_opts.updated = this.createSelection('div', this.visibleRows()).get('domid'); + } + + // Get split pane dimensions + switch (this.pane_mode) { + case 'horiz': + this._initSplitBar(); + + if (!size) { + this.page_size = (sp.horiz.loc && sp.horiz.loc > 0) + ? Math.min(sp.horiz.loc, this.getPageSize('splitmax')) + : this.getPageSize('default'); + } + sp.horiz.loc = this.page_size; + + if (sp.spacer) { + sp.spacer.hide(); + } + + h = lh * this.page_size; + c.setStyle({ height: h + 'px', width: '100%' }); + sp.currbar.show(); + this.opts.pane_data.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show(); + break; + + case 'vert': + this._initSplitBar(); + + if (!size) { + this.page_size = this.getPageSize('max'); + } + + if (!sp.vert.width) { + sp.vert.width = parseInt(this.opts.container.clientWidth * 0.35, 10); + } + + if (sp.spacer) { + sp.spacer.hide(); + } + + h = lh * this.page_size; + c.setStyle({ height: h + 'px', width: sp.vert.width + 'px' }); + sp.currbar.setStyle({ height: h + 'px' }).show(); + this.opts.pane_data.setStyle({ height: h + 'px' }).show(); + break; + + default: + if (sp.curr) { + if (this.pane_mode == 'horiz') { + sp.horiz.loc = this.page_size; + } + [ this.opts.pane_data, sp.currbar ].invoke('hide'); + sp.curr = sp.currbar = null; + } + + if (!size) { + this.page_size = this.getPageSize('max'); + } + + if (sp.spacer) { + sp.spacer.show(); + } else { + sp.spacer = new Element('DIV').setStyle({ clear: 'left' }); + this.opts.content.up().insert(sp.spacer); + } + + c.setStyle({ height: (lh * this.page_size) + 'px', width: '100%' }); + break; + } + + if (this.view) { + this.requestContentRefresh(this.currentOffset(), c_opts); + } + }, + + // offset = (integer) TODO + // opts = (object) See _updateContent() + requestContentRefresh: function(offset, opts) + { + if (!this._updateContent(offset, opts)) { + return false; + } + + var limit = this._getBuffer().isNearingLimit(offset); + if (limit) { + this._fetchBuffer({ + background: true, + nearing: limit, + offset: offset + }); + } + + return true; + }, + + // opts = (object) The following parameters: + // One of the following is REQUIRED: + // offset: (integer) Value of offset + // search: (object) List of search keys/values + // + // OPTIONAL: + // background: (boolean) Do fetch in background + // callback: (function) A callback to run when the request is complete + // initial: (boolean) Is this the initial access to this view? + // nearing: (string) TODO [only used w/offset] + // params: (object) Parameters to add to outgoing URL + // purge: (boolean) If true, purge the current rowlist and rebuild. + // Attempts to reuse the current data cache. + // view: (string) The view to retrieve. Defaults to current view. + _fetchBuffer: function(opts) + { + if (this.isbusy) { + return this._fetchBuffer.bind(this, opts).defer(); + } + + this.isbusy = true; + + var llist, lrows, rlist, tmp, type, value, + view = (opts.view || this.view), + b = this._getBuffer(view), + params = $H(opts.params), + r_id = this.request_num++; + + // Only fire fetch event if we are loading in foreground. + if (!opts.background) { + this.opts.container.fire('ViewPort:fetch', view); + } + + params.update({ requestid: r_id }); + + // Determine if we are querying via offset or a search query + if (opts.search || opts.initial || opts.purge) { + /* If this is an initial request, 'type' will be set correctly + * further down in the code. */ + if (opts.search) { + type = 'search'; + value = opts.search; + params.set('search', Object.toJSON(value)); + } + + if (opts.initial) { + params.set('initial', 1); + } + + if (opts.purge) { + b.resetRowlist(); + } + + tmp = this._lookbehind(); + + params.update({ + after: this.bufferSize() - tmp, + before: tmp + }); + } + + if (!opts.search) { + type = 'rownum'; + value = opts.offset + 1; + + // llist: keys - request_ids; vals - loading rownums + llist = b.getMetaData('llist') || $H(); + lrows = llist.values().flatten(); + + b.setMetaData({ req_offset: opts.offset }, true); + + /* If the current offset is part of a pending request, update + * the offset. */ + if (lrows.size() && + b.sliceLoaded(value, lrows)) { + /* One more hurdle. If we are loading in background, and now + * we are in foreground, we need to search for the request + * that contains the current rownum. For now, just use the + * last request. */ + if (!this.active_req && !opts.background) { + this.active_req = llist.keys().numericSort().last(); + } + this.isbusy = false; + return; + } + + /* This gets the list of rows needed which do not already appear + * in the buffer. */ + tmp = this._getSliceBounds(value, opts.nearing, view); + rlist = $A($R(tmp.start, tmp.end)).diff(b.getAllRows()); + + if (!rlist.size()) { + this.isbusy = false; + return; + } + + /* Add rows to the loading list for the view. */ + rlist = rlist.diff(lrows).numericSort(); + llist.set(r_id, rlist); + b.setMetaData({ llist: llist }, true); + + params.update({ slice: rlist.first() + ':' + rlist.last() }); + } + + if (opts.callback) { + tmp = b.getMetaData('callback') || $H(); + tmp.set(r_id, opts.callback); + b.setMetaData({ callback: tmp }, true); + } + + if (!opts.background) { + this.active_req = r_id; + this._handleWait(); + } + + this._ajaxRequest(params, { noslice: true, view: view }); + + this.isbusy = false; + }, + + // rownum = (integer) Row number + // nearing = (string) 'bottom', 'top', null + // view = (string) ID of view. + _getSliceBounds: function(rownum, nearing, view) + { + var b_size = this.bufferSize(), + ob = {}, trows; + + switch (nearing) { + case 'bottom': + ob.start = rownum + this.getPageSize(); + ob.end = ob.start + b_size; + break; + + case 'top': + ob.start = Math.max(rownum - b_size, 1); + ob.end = rownum; + break; + + default: + ob.start = rownum - this._lookbehind(); + + /* Adjust slice if it runs past edge of available rows. In this + * case, fetching a tiny buffer isn't as useful as switching + * the unused buffer space to the other endpoint. Always allow + * searching past the value of total_rows, since the size of the + * dataset may have increased. */ + trows = this.getMetaData('total_rows', view); + if (trows) { + ob.end = ob.start + b_size; + + if (ob.end > trows) { + ob.start -= ob.end - trows; + } + + if (ob.start < 1) { + ob.end += 1 - ob.start; + ob.start = 1; + } + } else { + ob.start = Math.max(ob.start, 1); + ob.end = ob.start + b_size; + } + break; + } + + return ob; + }, + + _lookbehind: function() + { + return parseInt((this.opts.lookbehind * 0.01) * this.bufferSize(), 10); + }, + + // args = (object) The list of parameters. + // opts = (object) [noslice, view] + // Returns a Hash object + addRequestParams: function(args, opts) + { + opts = opts || {}; + var cid = this.getMetaData('cacheid', opts.view), + cached, params, rowlist; + + params = this.opts.onAjaxRequest + ? this.opts.onAjaxRequest(opts.view || this.view) + : $H(); + + params.update({ view: opts.view || this.view }); + + if (cid) { + params.update({ cacheid: cid }); + } + + if (!opts.noslice) { + rowlist = this._getSliceBounds(this.currentOffset(), null, opts.view); + params.update({ slice: rowlist.start + ':' + rowlist.end }); + } + + if (this.opts.onCachedList) { + cached = this.opts.onCachedList(opts.view || this.view); + } else { + cached = this._getBuffer(opts.view).getAllUIDs(); + cached = cached.size() + ? cached.toJSON() + : ''; + } + + if (cached.length) { + params.update({ cache: cached }); + } + + return params.merge(args); + }, + + // params - (object) A list of parameters to send to server + // opts - (object) Args to pass to addRequestParams(). + _ajaxRequest: function(params, other) + { + new Ajax.Request(this.opts.ajax_url, Object.extend(this.opts.ajax_opts || {}, { + evalJS: false, + evalJSON: true, + onComplete: this.ajax_response, + onFailure: this.opts.onAjaxFailure || Prototype.emptyFunction, + parameters: this.addRequestParams(params, other) + })); + }, + + _ajaxRequestComplete: function(r) + { + if (r.responseJSON) { + this.parseJSONResponse(r.responseJSON); + } + }, + + // r - (object) responseJSON returned from the server. + parseJSONResponse: function(r) + { + if (!r.ViewPort) { + return; + } + + r = r.ViewPort; + + if (r.rangelist) { + this.select(this.createSelection('uid', r.rangelist, r.view)); + this.opts.container.fire('ViewPort:endFetch', r.view); + } + + if (!Object.isUndefined(r.updatecacheid)) { + this._getBuffer(r.view).setMetaData({ cacheid: r.updatecacheid }, true); + } else if (!Object.isUndefined(r.cacheid)) { + this._ajaxResponse(r); + } + }, + + // r = (Object) viewport response object + _ajaxResponse: function(r) + { + if (this.isbusy) { + this._ajaxResponse.bind(this, r).defer(); + return; + } + + this.isbusy = true; + this._clearWait(); + + var callback, offset, tmp, + buffer = this._getBuffer(r.view), + llist = buffer.getMetaData('llist') || $H(), + updated = []; + + buffer.update(Object.isArray(r.data) ? {} : r.data, Object.isArray(r.rowlist) ? {} : r.rowlist, r.metadata || {}, { reset: r.reset, resetmd: r.resetmd, update: r.update }); + + if (r.reset) { + this.select(new ViewPort_Selection()); + } else if (r.update && r.disappear && r.disappear.size()) { + this.deselect(this.createSelection('uid', r.disappear, r.view)); + buffer.removeData(r.disappear); + } + + llist.unset(r.requestid); + + tmp = { + cacheid: r.cacheid, + llist: llist, + total_rows: r.totalrows + }; + if (r.label) { + tmp.label = r.label; + } + buffer.setMetaData(tmp, true); + + this.opts.container.fire('ViewPort:cacheUpdate', r.view); + + if (r.requestid && + r.requestid == this.active_req) { + this.active_req = null; + callback = buffer.getMetaData('callback'); + offset = buffer.getMetaData('req_offset'); + + if (callback && callback.get(r.requestid)) { + callback.get(r.requestid)(r); + callback.unset(r.requestid); + } + + buffer.setMetaData({ callback: undefined, req_offset: undefined }, true); + + this.opts.container.fire('ViewPort:endFetch', r.view); + } + + if (this.view == r.view) { + if (r.update) { + updated = this.createSelection('uid', Object.keys(r.data)).get('domid'); + } + this._updateContent(Object.isUndefined(r.rownum) ? (Object.isUndefined(offset) ? this.currentOffset() : offset) : Number(r.rownum) - 1, { updated: updated }); + } else if (r.rownum) { + // We loaded in the background. If rownumber information was + // provided, we need to save this or else we will position the + // viewport incorrectly. + buffer.setMetaData({ offset: Number(r.rownum) - 1 }, true); + } + + this.isbusy = false; + }, + + // offset = (integer) TODO + // opts = (object) TODO [background, updated, view] + _updateContent: function(offset, opts) + { + opts = opts || {}; + + if (!this._getBuffer(opts.view).sliceLoaded(offset)) { + opts.offset = offset; + this._fetchBuffer(opts); + return false; + } + + var added = {}, + c = this.opts.content, + page_size = this.getPageSize(), + tmp = [], + updated = opts.updated || [], + vr = this.visibleRows(), + fdiv, rows; + + this.scroller.setSize(page_size, this.getMetaData('total_rows')); + this.scrollTo(offset + 1, { noupdate: true, top: true }); + + offset = this.currentOffset(); + if (this.opts.onContentOffset) { + offset = this.opts.onContentOffset(offset); + } + + rows = this.createSelection('rownum', $A($R(offset + 1, offset + page_size))); + + if (rows.size()) { + fdiv = document.createDocumentFragment().appendChild(new Element('DIV')); + + rows.get('dataob').each(function(r) { + var elt; + if (!updated.include(r.VP_domid) && + (elt = $(r.VP_domid))) { + tmp.push(elt); + } else { + fdiv.insert({ top: this.prepareRow(r) }); + added[r.VP_domid] = 1; + tmp.push(fdiv.down()); + } + }, this); + + vr.pluck('id').diff(rows.get('domid')).each($).compact().each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); + + c.childElements().invoke('remove'); + + tmp.each(function(r) { + c.insert(r); + if (added[r.identify()]) { + this.opts.container.fire('ViewPort:add', r); + } + }, this); + } else { + vr.each(this.opts.content.fire.bind(this.opts.content, 'ViewPort:clear')); + vr.invoke('remove'); + c.update(this.empty_msg); + } + + this.scroller.updateDisplay(); + this.opts.container.fire('ViewPort:contentComplete'); + + return true; + }, + + prepareRow: function(row) + { + var r = Object.clone(row); + + r.VP_bg = this.getSelected().contains('uid', r.VP_id) + ? [ 'vpRowSelected' ] + : []; + + return this.opts.onContent(r, this.pane_mode); + }, + + updateRow: function(row) + { + var d = $(row.VP_domid); + if (d) { + this.opts.container.fire('ViewPort:clear', d); + d.replace(this.prepareRow(row)); + this.opts.container.fire('ViewPort:add', $(row.VP_domid)); + } + }, + + _handleWait: function(call) + { + this._clearWait(); + + // Server did not respond in defined amount of time. Alert the + // callback function and set the next timeout. + if (call) { + this.opts.container.fire('ViewPort:wait', this.view); + } + + // Call wait handler every x seconds + if (this.opts.viewport_wait) { + this.waitHandler = this._handleWait.bind(this, true).delay(this.opts.viewport_wait); + } + }, + + _clearWait: function() + { + if (this.waitHandler) { + clearTimeout(this.waitHandler); + this.waitHandler = null; + } + }, + + visibleRows: function() + { + return this.opts.content.select('DIV.vpRow'); + }, + + getMetaData: function(id, view) + { + return this._getBuffer(view).getMetaData(id); + }, + + setMetaData: function(vals, view) + { + this._getBuffer(view).setMetaData(vals, false); + }, + + _getBuffer: function(view, create) + { + view = view || this.view; + + return (!create && this.views[view]) + ? this.views[view] + : new ViewPort_Buffer(this, view); + }, + + currentOffset: function() + { + return this.scroller.currentOffset(); + }, + + // return: (object) The current viewable range of the viewport. + // first: Top-most row offset + // last: Bottom-most row offset + currentViewableRange: function() + { + var offset = this.currentOffset(); + return { + first: offset + 1, + last: Math.min(offset + this.getPageSize(), this.getMetaData('total_rows')) + }; + }, + + _getLineHeight: function() + { + var mode = this.pane_mode || 'horiz'; + + if (!this.split_pane[mode].lh) { + // To avoid hardcoding the line height, create a temporary row to + // figure out what the CSS says. + var d = new Element('DIV', { className: this.opts.list_class }).insert(this.prepareRow({ VP_domid: null }, mode)).hide(); + $(document.body).insert(d); + this.split_pane[mode].lh = d.getHeight(); + d.remove(); + } + + return this.split_pane[mode].lh; + }, + + // (type) = (string) [null (DEFAULT), 'current', 'default', 'max'] + // return: (integer) Number of rows in current view. + getPageSize: function(type) + { + switch (type) { + case 'current': + return Math.min(this.page_size, this.getMetaData('total_rows')); + + case 'default': + return (this.pane_mode == 'vert') + ? this.getPageSize('max') + : Math.max(parseInt(this.getPageSize('max') * 0.45, 10), 5); + + case 'max': + case 'splitmax': + return parseInt(this._getMaxHeight() / this._getLineHeight()) - (type == 'max' ? 0 : 1); + + default: + return this.page_size; + } + }, + + _getMaxHeight: function() + { + return document.viewport.getHeight() - this.opts.content.viewportOffset()[1]; + }, + + bufferSize: function() + { + // Buffer size must be at least the maximum page size. + return Math.round(Math.max(this.getPageSize('max') + 1, this.opts.buffer_pages * this.getPageSize())); + }, + + limitTolerance: function() + { + return Math.round(this.bufferSize() * (this.opts.limit_factor / 100)); + }, + + // mode = (string) Either 'horiz', 'vert', or empty. + showSplitPane: function(mode) + { + this.pane_mode = mode; + this.onResize(true); + }, + + _initSplitBar: function() + { + var sp = this.split_pane; + + if (sp.currbar) { + sp.currbar.hide(); + } + + sp.curr = this.pane_mode; + + if (sp[this.pane_mode].bar) { + sp.currbar = sp[this.pane_mode].bar.show(); + return; + } + + sp.currbar = sp[this.pane_mode].bar = new Element('DIV', { className: this.opts.split_bar_class[this.pane_mode] }); + + if (!this.opts.pane_data.descendantOf(this.opts.container)) { + this.opts.container.insert(this.opts.pane_data.remove()); + } + + this.opts.pane_data.insert({ before: sp.currbar }); + + switch (this.pane_mode) { + case 'horiz': + new Drag(sp.currbar.setStyle({ clear: 'left' }), { + constraint: 'vertical', + ghosting: true, + nodrop: true, + snap: function(x, y, elt) { + var sp = this.split_pane, + l = parseInt((y - sp.pos) / sp.lh); + if (l < 1) { + l = 1; + } else if (l > sp.max) { + l = sp.max; + } + sp.lines = l; + return [ x, sp.pos + (l * sp.lh) ]; + }.bind(this) + }); + break; + + case 'vert': + new Drag(sp.currbar.setStyle({ float: 'left' }), { + constraint: 'horizontal', + ghosting: true, + nodrop: true, + snapToParent: true + }); + break; + } + + if (!sp.init) { + document.observe('DragDrop2:end', this._onDragEnd.bindAsEventListener(this)); + document.observe('DragDrop2:start', this._onDragStart.bindAsEventListener(this)); + document.observe('dblclick', this._onDragDblClick.bindAsEventListener(this)); + sp.init = true; + } + }, + + _onDragStart: function(e) + { + var sp = this.split_pane; + + if (e.element() != sp.currbar) { + return; + } + + if (this.pane_mode == 'horiz') { + // Cache these values since we will be using them multiple + // times in snap(). + sp.lh = this._getLineHeight(); + sp.lines = this.page_size; + sp.max = this.getPageSize('splitmax'); + sp.orig = this.page_size; + sp.pos = this.opts.content.positionedOffset()[1]; + } + + this.opts.container.fire('ViewPort:splitBarStart', this.pane_mode); + }, + + _onDragEnd: function(e) + { + var change, drag, + sp = this.split_pane; + + if (e.element() != sp.currbar) { + return; + } + + switch (this.pane_mode) { + case 'horiz': + this.onResize(true, sp.lines); + change = (sp.orig != sp.lines); + break; + + case 'vert': + drag = DragDrop.Drags.getDrag(e.element()); + sp.vert.width = drag.lastCoord[0]; + this.opts.content.setStyle({ width: sp.vert.width + 'px' }); + change = drag.wasDragged; + break; + } + + if (change) { + this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode); + } + this.opts.container.fire('ViewPort:splitBarEnd', this.pane_mode); + }, + + _onDragDblClick: function(e) + { + if (e.element() != this.split_pane.currbar) { + return; + } + + var change, old_size = this.page_size; + + switch (this.pane_mode) { + case 'horiz': + this.onResize(true, this.getPageSize('default')); + change = (old_size != this.page_size); + break; + + case 'vert': + this.opts.content.setStyle({ width: parseInt(this.opts.container.clientWidth * 0.45, 10) + 'px' }); + change = true; + } + + if (change) { + this.opts.container.fire('ViewPort:splitBarChange', this.pane_mode); + } + }, + + getAllRows: function(view) + { + var buffer = this._getBuffer(view); + return buffer + ? buffer.getAllRows() + : []; + }, + + createSelection: function(format, data, view) + { + var buffer = this._getBuffer(view); + return buffer + ? new ViewPort_Selection(buffer, format, data) + : new ViewPort_Selection(this._getBuffer(this.view)); + }, + + getSelection: function(view) + { + var buffer = this._getBuffer(view); + return this.createSelection('uid', buffer ? buffer.getSelected().get('uid') : [], view); + }, + + // vs = (Viewport_Selection | array) A Viewport_Selection object -or- if + // opts.range is set, an array of row numbers. + // opts = (object) TODO [add, range, search] + select: function(vs, opts) + { + opts = opts || {}; + + var b = this._getBuffer(), + sel, slice; + + if (opts.search) { + return this._fetchBuffer({ + callback: function(r) { + if (r.rownum) { + this.select(this.createSelection('rownum', [ r.rownum ]), { add: opts.add, range: opts.range }); + } + }.bind(this), + search: opts.search + }); + } + + if (opts.range) { + slice = this.createSelection('rownum', vs); + if (vs.size() != slice.size()) { + this.opts.container.fire('ViewPort:fetch', this.view); + return this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() }); + } + vs = slice; + } + + if (!opts.add) { + sel = this.getSelected(); + b.deselect(sel, true); + sel.get('div').invoke('removeClassName', 'vpRowSelected'); + } + b.select(vs); + vs.get('div').invoke('addClassName', 'vpRowSelected'); + this.opts.container.fire('ViewPort:select', { opts: opts, vs: vs }); + }, + + // vs = (Viewport_Selection) A Viewport_Selection object. + // opts = (object) TODO [clearall] + deselect: function(vs, opts) + { + opts = opts || {}; + + if (vs.size() && + this._getBuffer().deselect(vs, opts && opts.clearall)) { + vs.get('div').invoke('removeClassName', 'vpRowSelected'); + this.opts.container.fire('ViewPort:deselect', { opts: opts, vs: vs }); + } + }, + + getSelected: function() + { + return Object.clone(this._getBuffer().getSelected()); + } + +}), + +/** + * ViewPort_Scroller + */ +ViewPort_Scroller = Class.create({ + // Variables initialized to undefined: + // noupdate, scrollDiv, scrollbar, vertscroll, vp + + initialize: function(vp) + { + this.vp = vp; + }, + + _createScrollBar: function() + { + if (this.scrollDiv) { + return; + } + + var c = this.vp.opts.content; + + // Create the outer div. + this.scrollDiv = new Element('DIV', { className: 'vpScroll' }).setStyle({ float: 'left', overflow: 'hidden' }).hide(); + c.insert({ after: this.scrollDiv }); + + this.scrollDiv.observe('Slider2:change', this._onScroll.bind(this)); + if (this.vp.opts.onSlide) { + this.scrollDiv.observe('Slider2:slide', this.vp.opts.onSlide); + } + + // Create scrollbar object. + this.scrollbar = new Slider2(this.scrollDiv, { + buttonclass: { up: 'vpScrollUp', down: 'vpScrollDown' }, + cursorclass: 'vpScrollCursor', + pagesize: this.vp.getPageSize(), + totalsize: this.vp.getMetaData('total_rows') + }); + + // Mouse wheel handler. + c.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', function(e) { + var move_num = Math.min(this.vp.getPageSize(), 3); + this.moveScroll(this.currentOffset() + ((e.wheelDelta >= 0 || e.detail < 0) ? (-1 * move_num) : move_num)); + /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818 + * Need to stop or else multiple scroll events may be fired. We + * lose the ability to have the mousescroll bubble up, but that is + * more desirable than having the wrong scrolling behavior. */ + if (Prototype.Browser.Gecko && !e.stop) { + Event.stop(e); + } + }.bindAsEventListener(this)); + }, + + setSize: function(viewsize, totalsize) + { + this._createScrollBar(); + this.scrollbar.setHandleLength(viewsize, totalsize); + }, + + updateDisplay: function() + { + var c = this.vp.opts.content, + vs = false; + + if (this.scrollbar.needScroll()) { + switch (this.vp.pane_mode) { + case 'vert': + this.scrollDiv.setStyle({ marginLeft: 0 }); + if (!this.vertscroll) { + c.setStyle({ width: (c.clientWidth - this.scrollDiv.getWidth()) + 'px' }); + } + vs = true; + break; + + case 'horiz': + default: + this.scrollDiv.setStyle({ marginLeft: '-' + this.scrollDiv.getWidth() + 'px' }); + break; + } + + this.scrollDiv.setStyle({ height: c.clientHeight + 'px' }); + } else if ((this.vp.pane_mode == 'vert') && this.vertscroll) { + c.setStyle({ width: (c.clientWidth + this.scrollDiv.getWidth()) + 'px' }); + } + + this.vertscroll = vs; + this.scrollbar.updateHandleLength(); + }, + + clear: function() + { + this.setSize(0, 0); + this.scrollbar.updateHandleLength(); + }, + + // offset = (integer) Offset to move the scrollbar to + moveScroll: function(offset) + { + this._createScrollBar(); + this.scrollbar.setScrollPosition(offset); + }, + + _onScroll: function() + { + if (!this.noupdate) { + this.vp.requestContentRefresh(this.currentOffset()); + } + }, + + currentOffset: function() + { + return this.scrollbar ? this.scrollbar.getValue() : 0; + } + +}), + +/** + * ViewPort_Buffer + * + * Note: recognize the difference between offset (current location in the + * viewport - starts at 0) with start parameters (the row numbers - starts + * at 1). + */ +ViewPort_Buffer = Class.create({ + + initialize: function(vp, view) + { + this.vp = vp; + this.view = view; + this.clear(); + }, + + getView: function() + { + return this.view; + }, + + // d = (object) Data + // l = (object) Rowlist + // md = (object) User defined metadata + // opts = (object) TODO [reset, resetmd, update] + update: function(d, l, md, opts) + { + d = $H(d); + l = $H(l); + opts = opts || {}; + + if (!opts.reset && this.data.size()) { + this.data.update(d); + } else { + this.data = d; + } + + if (opts.update || opts.reset) { + this.uidlist = l; + this.rowlist = $H(); + } else { + this.uidlist = this.uidlist.size() ? this.uidlist.merge(l) : l; + } + + l.each(function(o) { + this.rowlist.set(o.value, o.key); + }, this); + + if (opts.resetmd) { + this.usermdata = $H(md); + } else { + $H(md).each(function(pair) { + if (Object.isString(pair.value) || + Object.isNumber(pair.value) || + Object.isArray(pair.value)) { + this.usermdata.set(pair.key, pair.value); + } else { + var val = this.usermdata.get(pair.key); + if (val) { + this.usermdata.get(pair.key).update($H(pair.value)); + } else { + this.usermdata.set(pair.key, $H(pair.value)); + } + } + }, this); + } + }, + + // offset = (integer) Offset of the beginning of the slice. + // rows = (array) Additional rows to include in the search. + sliceLoaded: function(offset, rows) + { + var range, tr = this.getMetaData('total_rows'); + + // Undefined here indicates we have never sent a previous buffer + // request. + if (Object.isUndefined(tr)) { + return false; + } + + range = $A($R(offset + 1, Math.min(offset + this.vp.getPageSize() - 1, tr))); + + return rows + ? (range.diff(this.rowlist.keys().concat(rows)).size() == 0) + : !this._rangeCheck(range); + }, + + isNearingLimit: function(offset) + { + if (this.uidlist.size() != this.getMetaData('total_rows')) { + if (offset != 0 && + this._rangeCheck($A($R(Math.max(offset + 1 - this.vp.limitTolerance(), 1), offset)))) { + return 'top'; + } else if (this._rangeCheck($A($R(offset + 1, Math.min(offset + this.vp.limitTolerance() + this.vp.getPageSize() - 1, this.getMetaData('total_rows')))).reverse())) { + // Search for missing rows in reverse order since in normal + // usage (sequential scrolling through the row list) rows are + // more likely to be missing at furthest from the current + // view. + return 'bottom'; + } + } + }, + + _rangeCheck: function(range) + { + return !range.all(this.rowlist.get.bind(this.rowlist)); + }, + + getData: function(uids) + { + return uids.collect(function(u) { + var e = this.data.get(u); + if (!Object.isUndefined(e)) { + // We can directly write the rownum to the original object + // since we will always rewrite when creating rows. + e.VP_domid = 'VProw' + this.view + '_' + u; + e.VP_rownum = this.uidlist.get(u); + e.VP_id = u; + return e; + } + }, this).compact(); + }, + + getAllUIDs: function() + { + return this.uidlist.keys(); + }, + + getAllRows: function() + { + return this.rowlist.keys(); + }, + + rowsToUIDs: function(rows) + { + return rows.collect(this.rowlist.get.bind(this.rowlist)).compact(); + }, + + // vs = (Viewport_Selection) TODO + select: function(vs) + { + this.selected.add('uid', vs.get('uid')); + }, + + // vs = (Viewport_Selection) TODO + // clearall = (boolean) Clear all entries? + deselect: function(vs, clearall) + { + var size = this.selected.size(); + + if (clearall) { + this.selected.clear(); + } else { + this.selected.remove('uid', vs.get('uid')); + } + return size != this.selected.size(); + }, + + getSelected: function() + { + return this.selected; + }, + + // rownums = (array) Array of row numbers to remove. + remove: function(rownums) + { + var minrow = rownums.min(), + rowsize = this.rowlist.size(), + rowsubtract = 0, + newsize = rowsize - rownums.size(); + + return this.rowlist.keys().each(function(n) { + if (n >= minrow) { + var id = this.rowlist.get(n), r; + if (rownums.include(n)) { + this.data.unset(id); + this.uidlist.unset(id); + rowsubtract++; + } else if (rowsubtract) { + r = n - rowsubtract; + this.rowlist.set(r, id); + this.uidlist.set(id, r); + } + if (n > newsize) { + this.rowlist.unset(n); + } + } + }, this); + }, + + removeData: function(uids) + { + uids.each(function(u) { + this.data.unset(u); + this.uidlist.unset(u); + }, this); + }, + + resetRowlist: function() + { + this.rowlist = $H(); + }, + + clear: function() + { + this.data = $H(); + this.mdata = $H({ total_rows: 0 }); + this.rowlist = $H(); + this.selected = new ViewPort_Selection(this); + this.uidlist = $H(); + this.usermdata = $H(); + }, + + getMetaData: function(id) + { + var data = this.mdata.get(id); + + return Object.isUndefined(data) + ? this.usermdata.get(id) + : data; + }, + + setMetaData: function(vals, priv) + { + if (priv) { + this.mdata.update(vals); + } else { + this.usermdata.update(vals); + } + } + +}), + +/** + * ViewPort_Selection + */ +ViewPort_Selection = Class.create({ + + // Define property to aid in object detection + viewport_selection: true, + + // Formats: + // 'dataob' = Data objects + // 'div' = DOM DIVs + // 'domid' = DOM IDs + // 'rownum' = Row numbers + // 'uid' = Unique IDs + initialize: function(buffer, format, data) + { + this.buffer = buffer; + this.clear(); + if (!Object.isUndefined(format)) { + this.add(format, data); + } + }, + + add: function(format, d) + { + var c = this._convert(format, d); + this.data = this.data.size() ? this.data.concat(c).uniq() : c; + }, + + remove: function(format, d) + { + this.data = this.data.diff(this._convert(format, d)); + }, + + _convert: function(format, d) + { + d = Object.isArray(d) ? d : [ d ]; + + // Data is stored internally as UIDs. + switch (format) { + case 'dataob': + return d.pluck('VP_id'); + + case 'div': + // ID here is the DOM ID of the element object. + d = d.pluck('id'); + // Fall-through + + case 'domid': + return d.invoke('substring', 6 + this.buffer.getView().length); + + case 'rownum': + return this.buffer.rowsToUIDs(d); + + case 'uid': + return d; + } + }, + + clear: function() + { + this.data = []; + }, + + get: function(format) + { + format = Object.isUndefined(format) ? 'uid' : format; + if (format == 'uid') { + return this.data; + } + var d = this.buffer.getData(this.data); + + switch (format) { + case 'dataob': + return d; + + case 'div': + return d.pluck('VP_domid').collect(function(e) { return $(e); }).compact(); + + case 'domid': + return d.pluck('VP_domid'); + + case 'rownum': + return d.pluck('VP_rownum'); + } + }, + + contains: function(format, d) + { + return this.data.include(this._convert(format, d).first()); + }, + + // params = (Object) Key is search key, value is object -> key of object + // must be the following: + // equal - Matches any value contained in the query array. + // include - Matches if this value is contained within the array. + // notequal - Matches any value not contained in the query array. + // notinclude - Matches if this value is not contained within the array. + // regex - Matches the RegExp contained in the query. + search: function(params) + { + return new ViewPort_Selection(this.buffer, 'uid', this.get('dataob').findAll(function(i) { + // i = data object + return $H(params).all(function(k) { + // k.key = search key; k.value = search criteria + return $H(k.value).all(function(s) { + // s.key = search type; s.value = search query + switch (s.key) { + case 'equal': + case 'notequal': + var r = i[k.key] && s.value.include(i[k.key]); + return (s.key == 'equal') ? r : !r; + + case 'include': + case 'notinclude': + var r = i[k.key] && Object.isArray(i[k.key]) && i[k.key].include(s.value); + return (s.key == 'include') ? r : !r; + + case 'regex': + return i[k.key].match(s.value); + } + }); + }); + }).pluck('VP_id')); + }, + + size: function() + { + return this.data.size(); + }, + + set: function(vals) + { + this.get('dataob').each(function(d) { + $H(vals).each(function(v) { + d[v.key] = v.value; + }); + }); + }, + + getBuffer: function() + { + return this.buffer; + } + +}); + +/** Utility Functions **/ +Object.extend(Array.prototype, { + // Need our own diff() function because prototypejs's without() function + // does not handle array input. + diff: function(values) + { + return this.select(function(value) { + return !values.include(value); + }); + }, + numericSort: function() + { + return this.collect(Number).sort(function(a, b) { + return (a > b) ? 1 : ((a < b) ? -1 : 0); + }); + } +}); diff --git a/imp/lib/Ajax/Application.php b/imp/lib/Ajax/Application.php index c30d09436..9fcb2568e 100644 --- a/imp/lib/Ajax/Application.php +++ b/imp/lib/Ajax/Application.php @@ -466,8 +466,8 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base *
            * 'checkcache' - (integer) If 1, only send data if cache has been
            *                invalidated.
      -     * 'rangeslice' - (string) Range slice. See js/ViewPort.js.
      -     * 'requestid' - (string) Request ID. See js/ViewPort.js.
      +     * 'rangeslice' - (string) Range slice. See js/viewport.js.
      +     * 'requestid' - (string) Request ID. See js/viewport.js.
            * 'sortby' - (integer) The Horde_Imap_Client sort constant.
            * 'sortdir' - (integer) 0 for ascending, 1 for descending.
            * 'view' - (string) The current full mailbox name.
      @@ -1695,7 +1695,7 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
           }
       
           /**
      -     * Generates the delete data needed for DimpBase.js.
      +     * Generates the delete data needed for dimpbase.js.
            *
            * See the list of variables needed for _viewPortData().
            *
      diff --git a/imp/lib/Dimp.php b/imp/lib/Dimp.php
      index f68b6ff6d..8210e582d 100644
      --- a/imp/lib/Dimp.php
      +++ b/imp/lib/Dimp.php
      @@ -58,8 +58,8 @@ class IMP_Dimp
               $core_scripts = array(
                   array('effects.js', 'horde'),
                   array('horde.js', 'horde'),
      -            array('DimpCore.js', 'imp'),
      -            array('Growler.js', 'horde')
      +            array('dimpcore.js', 'imp'),
      +            array('growler.js', 'horde')
               );
               foreach (array_merge($core_scripts, $scripts) as $val) {
                   call_user_func_array(array('Horde', 'addScriptFile'), $val);
      diff --git a/imp/message-dimp.php b/imp/message-dimp.php
      index c03e68b72..3a5c9a8e0 100644
      --- a/imp/message-dimp.php
      +++ b/imp/message-dimp.php
      @@ -56,8 +56,8 @@ if (isset($show_msg_result['error'])) {
       }
       
       $scripts = array(
      -    array('ContextSensitive.js', 'horde'),
      -    array('TextareaResize.js', 'horde'),
      +    array('contextsensitive.js', 'horde'),
      +    array('textarearesize.js', 'horde'),
           array('fullmessage-dimp.js', 'imp'),
           array('imp.js', 'imp'),
           array('md5.js', 'horde')
      diff --git a/jonah/channels/index.php b/jonah/channels/index.php
      index aa04165a7..0107773bc 100644
      --- a/jonah/channels/index.php
      +++ b/jonah/channels/index.php
      @@ -96,7 +96,7 @@ $template->set('notify', Horde::endBuffer());
       $title = _("Feeds");
       Horde::addScriptFile('prototype.js', 'horde', true);
       Horde::addScriptFile('tables.js', 'horde', true);
      -Horde::addScriptFile('QuickFinder.js', 'horde', true);
      +Horde::addScriptFile('quickfinder.js', 'horde', true);
       require JONAH_TEMPLATES . '/common-header.inc';
       echo $template->fetch(JONAH_TEMPLATES . '/channels/index.html');
       require $registry->get('templates', 'horde') . '/common-footer.inc';
      diff --git a/kronolith/lib/Kronolith.php b/kronolith/lib/Kronolith.php
      index e2c0e5b7c..fadd0ab7d 100644
      --- a/kronolith/lib/Kronolith.php
      +++ b/kronolith/lib/Kronolith.php
      @@ -69,8 +69,8 @@ class Kronolith
               Horde::addScriptFile('sound.js', 'horde');
               Horde::addScriptFile('horde.js', 'horde');
               Horde::addScriptFile('dragdrop2.js', 'horde');
      -        Horde::addScriptFile('Growler.js', 'horde');
      -        Horde::addScriptFile('dhtmlHistory.js', 'horde');
      +        Horde::addScriptFile('growler.js', 'horde');
      +        Horde::addScriptFile('dhtmlhistory.js', 'horde');
               Horde::addScriptFile('redbox.js', 'horde');
               Horde::addScriptFile('tooltips.js', 'horde');
               Horde::addScriptFile('colorpicker.js', 'horde');
      diff --git a/kronolith/templates/panel.inc b/kronolith/templates/panel.inc
      index 80bc4621c..0762f782a 100644
      --- a/kronolith/templates/panel.inc
      +++ b/kronolith/templates/panel.inc
      @@ -7,7 +7,7 @@ function toggleTags(domid)
       }
       
       notify();
      diff --git a/mnemo/notes/index.php b/mnemo/notes/index.php
      index 7d01021b7..228c83880 100644
      --- a/mnemo/notes/index.php
      +++ b/mnemo/notes/index.php
      @@ -40,7 +40,7 @@ $memos = $search_results;
       Horde::addScriptFile('tooltips.js', 'horde', true);
       Horde::addScriptFile('tables.js', 'horde', true);
       Horde::addScriptFile('prototype.js', 'horde', true);
      -Horde::addScriptFile('QuickFinder.js', 'horde', true);
      +Horde::addScriptFile('quickfinder.js', 'horde', true);
       
       require MNEMO_TEMPLATES . '/common-header.inc';
       require MNEMO_TEMPLATES . '/menu.inc';
      diff --git a/nag/list.php b/nag/list.php
      index df3d74a49..d0df3a8f1 100644
      --- a/nag/list.php
      +++ b/nag/list.php
      @@ -87,7 +87,7 @@ default:
       
       Horde::addScriptFile('tooltips.js', 'horde');
       Horde::addScriptFile('effects.js', 'horde');
      -Horde::addScriptFile('QuickFinder.js', 'horde');
      +Horde::addScriptFile('quickfinder.js', 'horde');
       
       require NAG_TEMPLATES . '/common-header.inc';
       require NAG_TEMPLATES . '/menu.inc';
      diff --git a/nag/tasks/index.php b/nag/tasks/index.php
      index b8a0de564..16dfc98e1 100644
      --- a/nag/tasks/index.php
      +++ b/nag/tasks/index.php
      @@ -47,7 +47,7 @@ $actionID = null;
       
       Horde::addScriptFile('tooltips.js', 'horde');
       Horde::addScriptFile('effects.js', 'horde');
      -Horde::addScriptFile('QuickFinder.js', 'horde');
      +Horde::addScriptFile('quickfinder.js', 'horde');
       
       require NAG_TEMPLATES . '/common-header.inc';
       require NAG_TEMPLATES . '/menu.inc';
      diff --git a/skoli/list.php b/skoli/list.php
      index 9072af195..4dddfaeb1 100644
      --- a/skoli/list.php
      +++ b/skoli/list.php
      @@ -100,7 +100,7 @@ default:
       
       Horde::addScriptFile('tooltips.js', 'horde');
       Horde::addScriptFile('effects.js', 'horde');
      -Horde::addScriptFile('QuickFinder.js', 'horde');
      +Horde::addScriptFile('quickfinder.js', 'horde');
       
       require SKOLI_TEMPLATES . '/common-header.inc';
       require SKOLI_TEMPLATES . '/menu.inc';
      diff --git a/skoli/search.php b/skoli/search.php
      index 551a5b8c7..e2a5e856b 100644
      --- a/skoli/search.php
      +++ b/skoli/search.php
      @@ -120,7 +120,7 @@ if ($conf['objects']['allow_absences']) {
       $title = _("Search");
       $notification->push('document.skoli_searchform.stext.focus();', 'javascript');
       
      -Horde::addScriptFile('QuickFinder.js', 'horde');
      +Horde::addScriptFile('quickfinder.js', 'horde');
       Horde::addScriptFile('effects.js', 'horde');
       Horde::addScriptFile('redbox.js', 'horde');
       require SKOLI_TEMPLATES . '/common-header.inc';
      diff --git a/skoli/templates/panel.inc b/skoli/templates/panel.inc
      index e67660e42..ead9ed579 100644
      --- a/skoli/templates/panel.inc
      +++ b/skoli/templates/panel.inc
      @@ -1,5 +1,5 @@
       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';