Cleanup of ViewPort.
authorMichael M Slusarz <slusarz@curecanti.org>
Wed, 15 Apr 2009 20:52:25 +0000 (14:52 -0600)
committerMichael M Slusarz <slusarz@curecanti.org>
Thu, 16 Apr 2009 05:10:30 +0000 (23:10 -0600)
Start to document things better.
Do more processing internal to ViewPort - such as doing own Ajax
requests, using standardized CSS class names, less mandatory config
options (down to only 3).
Remove some duplicative/unuseful callbacks.
Cache some frequently used callbacks/Elements.
Store offset when switching views in buffer metadata.

imp/ajax.php
imp/js/src/DimpBase.js
imp/js/src/DimpCore.js
imp/js/src/ViewPort.js
imp/lib/DIMP.php
imp/lib/Views/ListMessages.php
imp/templates/index/index-dimp.inc
imp/templates/javascript/mailbox-dimp.js
imp/themes/screen-dimp.css
imp/themes/screen.css

index e87a240..12f8dfd 100644 (file)
@@ -29,7 +29,7 @@ function _generateDeleteResult($mbox, $indices, $change, $nothread = false)
     }
 
     if ($change) {
-        $result->viewport = _getListMessages($mbox, true);
+        $result->ViewPort = _getListMessages($mbox, true);
     }
 
     $poll = _getPollInformation($mbox);
@@ -104,7 +104,7 @@ function _getListMessages($mbox, $change)
 
     $req_id = Util::getPost('request_id');
     if (!is_null($req_id)) {
-        $res->request_id = $req_id;
+        $res->request_id = intval($req_id);
     }
 
     return $res;
@@ -309,7 +309,7 @@ case 'PollFolders':
     }
 
     if (!empty($mbox) && _changed($mbox, $cacheid)) {
-        $result->viewport = _getListMessages($mbox, true);
+        $result->ViewPort = _getListMessages($mbox, true);
     }
 
     $quota = _getQuota();
@@ -318,7 +318,7 @@ case 'PollFolders':
     }
     break;
 
-case 'ListMessages':
+case 'ViewPort':
     if (empty($mbox)) {
         break;
     }
@@ -336,7 +336,7 @@ case 'ListMessages':
     if (Util::getPost('rangeslice') ||
         !Util::getPost('checkcache') ||
         $changed) {
-        $result->viewport = _getListMessages($mbox, $changed);
+        $result->ViewPort = _getListMessages($mbox, $changed);
     }
     break;
 
index 56e6f7e..202c3ec 100644 (file)
@@ -383,31 +383,66 @@ var DimpBase = {
 
     _createViewPort: function()
     {
-        // No need to cache - this function only called once.
-        var settitle = this.setMessageListTitle.bind(this);
-
         this.viewport = new ViewPort({
-            content_container: 'msgList',
-            empty_container: 'msgList_empty',
-            error_container: 'msgList_error',
-            fetch_action: 'ListMessages',
+            // Mandatory config
+            ajax_url: DIMP.conf.URI_IMP + '/ViewPort',
+            content: 'msgList',
             template: this.message_list_template,
+
+            // Optional config
+            ajax_opts: DimpCore.doActionOpts,
             buffer_pages: DIMP.conf.buffer_pages,
+            content_class: 'msglist',
+            empty_msg: DIMP.text.vp_empty,
+            error_msg: DIMP.text.vp_error,
             limit_factor: DIMP.conf.limit_factor,
-            viewport_wait: DIMP.conf.viewport_wait,
+            page_size: DIMP.conf.splitbar_pos,
             show_split_pane: DIMP.conf.preview_pref,
+            split_bar: 'splitBar',
             split_pane: 'previewPane',
-            splitbar: 'splitBar',
-            content_class: 'msglist',
-            row_class: 'msgRow',
-            selected_class: 'selectedRow',
-            ajaxRequest: DimpCore.doAction.bind(DimpCore),
-            norows: true,
-            page_size: DIMP.conf.splitbar_pos,
-            onScrollIdle: settitle,
-            onSlide: settitle,
+            wait: DIMP.conf.viewport_wait,
+
+            // Callbacks
+            onAjaxRequest: function(id) {
+                var p = this.isSearch(id)
+                    ? $H({
+                        qsearch: $F('quicksearch'),
+                        qsearchmbox: this.sfolder
+                    })
+                    : $H();
+                return DimpCore.addRequestParams(p);
+            }.bind(this),
+            onAjaxResponse: DimpCore.doActionComplete.bind(DimpCore),
+            onCachedList: function(id) {
+                var tmp, vs;
+                if (!this.cacheids[id]) {
+                    vs = this.viewport.getSelection(id, true);
+                    if (!vs.size()) {
+                        return '';
+                    }
+
+                    if (vs.getBuffer().getMetaData('search')) {
+                        this.cacheids[id] = vs.get('uid').toJSON();
+                    } else {
+                        tmp = {};
+                        tmp[id] = vs.get('uid').clone();
+                        this.cacheids[id] = DimpCore.toRangeString(tmp);
+                    }
+                }
+                return this.cacheids[id];
+            }.bind(this),
+            onCacheUpdate: function(id) {
+                delete this.cacheids[id];
+            }.bind(this),
+            onClearRows: function(r) {
+                r.each(function(row) {
+                    if (row.readAttribute('id')) {
+                        this._removeMouseEvents(row);
+                    }
+                }, this);
+            }.bind(this),
             onContent: function(row) {
-                var bg, re, search, u,
+                var bg, re, u,
                     thread = this.viewport.getMetaData('thread') || $H();
 
                 row.subjectdata = row.status = '';
@@ -426,29 +461,31 @@ var DimpBase = {
                 }
 
                 /* Generate the status flags. */
-                row.flag.each(function(a) {
-                    var ptr = DIMP.conf.flags[a];
-                    if (ptr.p) {
-                        if (!ptr.elt) {
-                            /* Until text-overflow is supported on all
-                             * browsers, need to truncate label text
-                             * ourselves. */
-                            ptr.elt = '<span class="' + ptr.c + '" title="' + ptr.l + '" style="background:' + ptr.b + '">' + ptr.l.truncate(10) + '</span>';
-                        }
-                        row.subjectdata += ptr.elt;
-                    } else {
-                        if (!ptr.elt) {
-                            ptr.elt = '<div class="msgflags ' + ptr.c + '" title="' + ptr.l + '"></div>';
+                if (row.flag) {
+                    row.flag.each(function(a) {
+                        var ptr = DIMP.conf.flags[a];
+                        if (ptr.p) {
+                            if (!ptr.elt) {
+                                /* Until text-overflow is supported on all
+                                 * browsers, need to truncate label text
+                                 * ourselves. */
+                                ptr.elt = '<span class="' + ptr.c + '" title="' + ptr.l + '" style="background:' + ptr.b + '">' + ptr.l.truncate(10) + '</span>';
+                            }
+                            row.subjectdata += ptr.elt;
+                        } else {
+                            if (!ptr.elt) {
+                                ptr.elt = '<div class="msgflags ' + ptr.c + '" title="' + ptr.l + '"></div>';
+                            }
+                            row.status += ptr.elt;
+
+                            row.bg.push(ptr.c);
+
+                            if (ptr.b) {
+                                bg = ptr.b;
+                            }
                         }
-                        row.status += ptr.elt;
-
-                        row.bg_string += ' ' + ptr.c;
-
-                        if (ptr.b) {
-                            bg = ptr.b;
-                        }
-                    }
-                });
+                    });
+                }
 
                 // Set bg
                 if (bg) {
@@ -549,60 +586,25 @@ var DimpBase = {
 
                 this.setSortColumns(ssc);
             }.bind(this),
-            onFetch: this.msgListLoading.bind(this, true),
+            onDeselect: this._deselect.bind(this),
             onEndFetch: this.msgListLoading.bind(this, false),
-            onCacheUpdate: function(id) {
-                delete this.cacheids[id];
-            }.bind(this),
-            onWait: function() {
-                if ($('dimpmain_folder').visible()) {
-                    DimpCore.showNotifications([ { type: 'horde.warning', message: DIMP.text.listmsg_wait } ]);
-                }
-            },
             onFail: function() {
                 if ($('dimpmain_folder').visible()) {
                     DimpCore.showNotifications([ { type: 'horde.error', message: DIMP.text.listmsg_timeout } ]);
                 }
                 this.msgListLoading(false);
             }.bind(this),
-            onClearRows: function(r) {
-                r.each(function(row) {
-                    if (row.readAttribute('id')) {
-                        this._removeMouseEvents(row);
-                    }
-                }, this);
-            }.bind(this),
-            onCachedList: function(id) {
-                var tmp, vs;
-                if (!this.cacheids[id]) {
-                    vs = this.viewport.getSelection(id, true);
-                    if (!vs.size()) {
-                        return '';
-                    }
-
-                    if (vs.getBuffer().getMetaData('search')) {
-                        this.cacheids[id] = vs.get('uid').toJSON();
-                    } else {
-                        tmp = {};
-                        tmp[id] = vs.get('uid').clone();
-                        this.cacheids[id] = DimpCore.toRangeString(tmp);
-                    }
-                }
-                return this.cacheids[id];
-            }.bind(this),
-            requestParams: function(id) {
-                return this.isSearch(id)
-                    ? $H({
-                        qsearch: $F('quicksearch'),
-                        qsearchmbox: this.sfolder
-                    })
-                    : $H();
-            }.bind(this),
+            onFetch: this.msgListLoading.bind(this, true),
+            onSelect: this._select.bind(this),
+            onSlide: this.setMessageListTitle.bind(this),
             onSplitBarChange: function() {
                 this._updatePrefs('dimp_splitbar', this.viewport.getPageSize());
             }.bind(this),
-            selectCallback: this._select.bind(this),
-            deselectCallback: this._deselect.bind(this)
+            onWait: function() {
+                if ($('dimpmain_folder').visible()) {
+                    DimpCore.showNotifications([ { type: 'horde.warning', message: DIMP.text.listmsg_wait } ]);
+                }
+            }
         });
 
         // If starting in no preview mode, need to set the no preview class
@@ -1449,8 +1451,8 @@ var DimpBase = {
         var elt = e.element(),
             tmp;
 
-        if (!elt.hasClassName('msgRow')) {
-            elt = elt.up('.msgRow');
+        if (!elt.hasClassName('vpRow')) {
+            elt = elt.up('.vpRow');
         }
 
         if (elt) {
@@ -2126,7 +2128,7 @@ var DimpBase = {
 
     hasFlag: function(f, r)
     {
-        return this.convertFlag(f, r.flag.include(f));
+        return r.flag && this.convertFlag(f, r.flag.include(f));
     },
 
     convertFlag: function(f, set)
@@ -2158,7 +2160,10 @@ var DimpBase = {
 
     _updateFlag: function(ob, flag, add)
     {
-        ob.flag = ob.flag.without(flag);
+        ob.flag = ob.flag
+            ? ob.flag.without(flag)
+            : [];
+
         if (add) {
             ob.flag.push(flag);
         }
@@ -2453,8 +2458,8 @@ DimpBase._folderDropConfig = {
 /* Need to register a callback function for doAction to catch viewport
  * information returned from the server. */
 DimpCore.onDoActionComplete = function(r) {
-    if (DimpBase.viewport && r.response.viewport) {
-        DimpBase.viewport.ajaxResponse(r.response.viewport);
+    if (DimpBase.viewport) {
+        DimpBase.viewport.parseJSONResponse(r);
     }
 };
 
index 90e5845..ac597f1 100644 (file)
@@ -116,6 +116,7 @@ var DimpCore = {
         action = action.startsWith('*')
             ? action.substring(1)
             : DIMP.conf.URI_IMP + '/' + action;
+
         if (uids) {
             if (uids.viewport_selection) {
                 b = uids.getBuffer();
@@ -133,14 +134,24 @@ var DimpCore = {
             }
             params.set('uid', this.toRangeString(uids));
         }
-        if (DIMP.conf.SESSION_ID) {
-            params.update(DIMP.conf.SESSION_ID.toQueryParams());
-        }
-        opts.parameters = params.toQueryString();
+
+        opts.parameters = this.addRequestParams(params);
         opts.onComplete = function(t, o) { this.doActionComplete(t, callback); }.bind(this);
         new Ajax.Request(action, opts);
     },
 
+    // 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;
@@ -174,8 +185,8 @@ var DimpCore = {
 
         this.showNotifications(r.msgs);
 
-        if (this.onDoActionComplete) {
-            this.onDoActionComplete(r);
+        if (r.response && this.onDoActionComplete) {
+            this.onDoActionComplete(r.response);
         }
 
         this.inAjaxCallback = false;
@@ -347,7 +358,7 @@ var DimpCore = {
             return;
         }
 
-        var elt = e.element(), id, opts, tmp;
+        var elt = e.element(), id, tmp;
 
         while (Object.isElement(elt)) {
             id = elt.readAttribute('id');
index 65db473..8929134 100644 (file)
@@ -1,6 +1,60 @@
 /**
  * ViewPort.js - Code to create a viewport window in a web browser.
  *
+ * Usage:
+ * ======
+ * var viewport = new ViewPort({ options });
+ *
+ * Required options:
+ * -----------------
+ * ajax_url: (string) TODO
+ *           Response: 'ViewPort'
+ * content: (Element/string) A DOM element/ID of the container to hold the
+ *          viewport rows.
+ * template: (string) TODO DIV with 'vpData'
+ *                    Class: 'vpRow' 'vpRowSelected'
+ *
+ * Optional options:
+ * -----------------
+ * ajax_opts: (object) TODO
+ * 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'.
+ * error_msg: (string) A string to display when an error is encountered.
+ *            Inserted in a SPAN element with class 'vpError'.
+ * 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.
+ * page_size: (integer) Default page size to view on load.
+ * show_split_pane: (boolean) Show the split pane on load?
+ * split_bar: (Element/string) A DOM element/ID of the element used to display
+ *            the split bar.
+ * split_pane: (Element/string) A DOM element/ID of the container to hold
+ *             the split pane info.
+ * wait: (integer) How long, in seconds, to wait before displaying an
+ *       informational message to users that the message list is still being
+ *       built.
+ *
+ * Callbacks:
+ * ----------
+ * onAjaxRequest
+ * onAjaxResponse
+ * onCachedList
+ * onCacheUpdate
+ * onClearRows
+ * onContent
+ * onContentComplete
+ * onDeselect
+ * onEndFetch
+ * onFail
+ * onFetch
+ * onSelect
+ * onSlide
+ * onSplitBarChange
+ * onWait
+ *
  * Requires prototypejs 1.6+, DimpSlider.js, scriptaculous 1.8+ (effects.js
  * only), and Horde's dragdrop2.js.
  *
  */
 var ViewPort = Class.create({
 
-    // Required: content_container, fetch_action, template,
-    //           cachecheck_action, ajaxRequest, buffer_pages,
-    //           limit_factor, content_class, row_class, selected_class
-    // Optional: requestParams, show_split_pane, page_size
     initialize: function(opts)
     {
-        opts.content = $(opts.content_container);
-        opts.empty = opts.empty_container ? $(opts.empty_container) : null;
-        opts.error = opts.error_container ? $(opts.error_container) : null;
-        this.opts = opts;
+        this.opts = Object.extend({
+            buffer_pages: 5,
+            limit_factor: 35
+        }, opts);
+
+        this.opts.content = $(opts.content);
+        this.opts.split_pane = $(opts.split_pane);
 
         this.scroller = new ViewPort_Scroller(this);
         this.template = new Template(opts.template);
 
-        this.current_req_lookup = $H();
-        this.current_req = $H();
-        this.fetch_hash = $H();
-        this.views = $H();
+        this.current_req_lookup = {};
+        this.current_req = {};
+        this.fetch_hash = {};
+        this.views = {};
 
-        this.splitbar_loc = opts.page_size;
-        this.showSplitPane(opts.show_split_pane);
+        this.split_bar_loc = opts.page_size;
+        this.show_split_pane = opts.show_split_pane;
 
-        this.isbusy = this.line_height = this.old_page_size = this.page_size = this.splitbar = this.viewport_init = null;
+        this.isbusy = this.line_height = this.page_size = this.split_bar = this.viewport_init = 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);
     },
 
     // view = ID of view.
@@ -55,8 +114,7 @@ var ViewPort = Class.create({
         if (this.page_size === null) {
             ps = this.getPageSize(this.show_split_pane ? 'default' : 'max');
             if (isNaN(ps)) {
-                this.loadView.bind(this, view, search, background).defer();
-                return;
+                return this.loadView.bind(this, view, search, background).defer();
             }
             this.page_size = ps;
         }
@@ -64,7 +122,9 @@ var ViewPort = Class.create({
         if (this.view) {
             if (!background) {
                 // Need to store current buffer to save current offset
-                this.views.set(this.view, { buffer: this._getBuffer(), offset: this.currentOffset() });
+                buffer = this._getBuffer();
+                buffer.setMetaData({ offset: this.currentOffset() }, true);
+                this.views[this.view] = buffer;
             }
             init = false;
         }
@@ -79,26 +139,25 @@ var ViewPort = Class.create({
             }
         }
 
-        curr = this.views.get(view);
+        curr = this.views[view];
 
         if (curr) {
-            this._updateContent(curr.offset, opts);
+            this._updateContent(curr.getMetaData('offset') || 0, opts);
             if (!background) {
-                this.opts.ajaxRequest(this.opts.fetch_action, this.addRequestParams({ checkcache: 1, rownum: this.currentOffset() + 1 }));
+                this._ajaxRequest({ checkcache: 1, rownum: this.currentOffset() + 1 });
             }
-            return true;
+            return;
         }
 
         if (!init) {
             if (this.opts.onClearRows) {
-                this.opts.onClearRows(this.opts.content.childElements());
+                this.opts.onClearRows(this.visibleRows());
             }
             this.opts.content.update();
             this.scroller.clear();
         }
 
-        buffer = this._getBuffer(view, true);
-        this.views.set(view, { buffer: buffer, offset: 0 });
+        this.views[view] = buffer = this._getBuffer(view, true);
 
         if (search) {
             opts.search = search;
@@ -106,14 +165,12 @@ var ViewPort = Class.create({
             opts.offset = 0;
         }
         this._fetchBuffer(opts);
-
-        return false;
     },
 
     // view = ID of view
     deleteView: function(view)
     {
-        this.views.unset(view);
+        delete this.views[view];
     },
 
     // rownum = (integer) Row number
@@ -148,8 +205,9 @@ var ViewPort = Class.create({
     isVisible: function(rownum)
     {
         var offset = this.currentOffset();
-        return (rownum < offset + 1) ? -1 :
-               ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0);
+        return (rownum < offset + 1)
+            ? -1
+            : ((rownum > (offset + this.getPageSize('current'))) ? 1 : 0);
     },
 
     // params = TODO
@@ -230,8 +288,8 @@ var ViewPort = Class.create({
     // nowait = (boolean) TODO
     onResize: function(noupdate, nowait)
     {
-        if (!this.opts.content.childElements().size() ||
-            !this.opts.content.visible()) {
+        if (!this.opts.content.visible() ||
+            !this.visibleRows().size()) {
             return;
         }
 
@@ -240,22 +298,9 @@ var ViewPort = Class.create({
         }
 
         if (nowait) {
-            this._onResize(noupdate);
+            this._renderViewport(noupdate);
         } else {
-            this.resizefunc = this._onResize.bind(this, noupdate).delay(0.1);
-        }
-    },
-
-    _onResize: function(noupdate)
-    {
-        if (this.opts.onBeforeResize) {
-            this.opts.onBeforeResize();
-        }
-
-        this._renderViewport(noupdate);
-
-        if (this.opts.onAfterResize) {
-            this.opts.onAfterResize();
+            this.resizefunc = this._renderViewport.bind(this, noupdate).delay(0.1);
         }
     },
 
@@ -288,7 +333,7 @@ var ViewPort = Class.create({
     //
     // Outgoing request has the following params:
     //   rownum: (integer) TODO
-    //   request_id: (string) TODO
+    //   request_id: (integer) TODO
     //   rows: (JSON array) TODO [optional]
     //
     //   search: (JSON object)
@@ -309,17 +354,15 @@ var ViewPort = Class.create({
         }
 
         var view = (opts.view || this.view),
-            action = this.opts.fetch_action,
             allrows,
             b = this._getBuffer(view),
             cr,
-            lb,
             params = $H(opts.params),
             request_id,
             request_string,
             request_old,
             rlist,
-            rowlist,
+            tmp,
             type,
             value,
             viewable;
@@ -333,9 +376,9 @@ var ViewPort = Class.create({
         if (opts.search) {
             type = 'search';
             value = opts.search;
-            lb = this._lookbehind(view);
+            tmp = this._lookbehind(view);
 
-            params.update({ search_before: lb, search_after: b.bufferSize() - lb });
+            params.update({ search_before: tmp, search_after: b.bufferSize() - tmp });
         } else {
             type = 'rownum';
             value = opts.offset + 1;
@@ -343,8 +386,9 @@ var ViewPort = Class.create({
             // This gets the list of rows needed which do not already appear
             // in the buffer.
             allrows = b.getAllRows(true);
-            rowlist = opts.rowlist ? opts.rowlist : this._getSliceBounds(value, opts.nearing, view);
-            rlist = $A($R(rowlist.start, rowlist.end)).diff(allrows);
+            tmp = opts.rowlist || this._getSliceBounds(value, opts.nearing, view);
+            rlist = $A($R(tmp.start, tmp.end)).diff(allrows);
+
             if (!opts.purge && !rlist.size()) {
                 this.isbusy = false;
                 return;
@@ -358,11 +402,11 @@ var ViewPort = Class.create({
         // Since javascript does not have a native hashing function, use a
         // local lookup table instead.
         request_string = [ view, type, value ].toJSON();
-        request_id = this.fetch_hash.get(request_string);
+        request_id = this.fetch_hash[request_string];
 
         // If we have a current request pending in the current view, figure
         // out if we need to send a new request
-        cr = this.current_req.get(view);
+        cr = this.current_req[view];
         if (cr) {
             if (request_id && cr.get(request_id)) {
                 // Check for repeat request.  We technically should never
@@ -413,12 +457,12 @@ var ViewPort = Class.create({
         }
 
         if (!request_id) {
-            request_id = this.fetch_hash.set(request_string, this.request_num++);
+            request_id = this.fetch_hash[request_string] = this.request_num++;
         }
         params.set('request_id', request_id);
         this._addRequest(view, request_id, { background: opts.background, offset: value - 1, rlist: rlist });
 
-        this.opts.ajaxRequest(action, this.addRequestParams(params, { noslice: true, view: view }));
+        this._ajaxRequest(params, { noslice: true, view: view });
         this._handleWait();
         this.isbusy = false;
     },
@@ -466,8 +510,8 @@ var ViewPort = Class.create({
         var cid = this.getMetaData('cacheid', opts.view),
             cached, params, rowlist;
 
-        params = this.opts.requestParams
-            ? this.opts.requestParams(opts.view || this.view)
+        params = this.opts.onAjaxRequest
+            ? this.opts.onAjaxRequest(opts.view || this.view)
             : $H();
 
         params.update({ view: opts.view || this.view });
@@ -497,10 +541,36 @@ var ViewPort = Class.create({
         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,
+            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) {
+            this._ajaxResponse(r.ViewPort);
+        }
+    },
+
     // r = (Object) viewport response object. Properties:
     //     cacheid
     //     data
-    //     id
     //     label
     //     metadata (optional)
     //     partial (optional) - TODO
@@ -512,10 +582,11 @@ var ViewPort = Class.create({
     //     totalrows
     //     update (optional) - If set, update the rowlist instead of
     //                         overwriting it.
-    ajaxResponse: function(r)
+    //     view
+    _ajaxResponse: function(r)
     {
         if (this.isbusy) {
-            this.ajaxResponse.bind(this, r).defer();
+            this._ajaxResponse.bind(this, r).defer();
             return;
         }
 
@@ -523,18 +594,14 @@ var ViewPort = Class.create({
         this._clearWait();
 
         var buffer, cr_id,
-            id = (r.request_id) ? this.current_req_lookup.get(r.request_id) : r.id,
-            cr = this.current_req.get(id);
+            view = r.request_id ? this.current_req_lookup[r.request_id] : r.view,
+            cr = this.current_req[view];
 
         if (cr && r.request_id) {
             cr_id = cr.get(r.request_id);
         }
 
-        if (this.viewport_init) {
-            this.viewport_init = 2;
-        }
-
-        buffer = this._getBuffer(id);
+        buffer = this._getBuffer(view);
         buffer.update(Object.isArray(r.data) ? {} : r.data, Object.isArray(r.rowlist) ? {} : r.rowlist, r.metadata || {}, { partial: r.partial, reset: r.reset, resetmd: r.resetmd, update: r.update });
 
         if (!r.partial) {
@@ -546,21 +613,21 @@ var ViewPort = Class.create({
         }
 
         if (this.opts.onCacheUpdate) {
-            this.opts.onCacheUpdate(id);
+            this.opts.onCacheUpdate(view);
         }
 
         if (r.request_id) {
-            this._removeRequest(id, r.request_id);
+            this._removeRequest(view, r.request_id);
         }
 
         this.isbusy = false;
 
         if (r.partial) {
-            this.select(this.getSelection(id));
+            this.select(this.getSelection(view));
         } else {
             // Don't update the viewport if we are now in a different view, or
             // if we are loading in the background.
-            if (!(this.view == id || r.search) ||
+            if (!(this.view == view || r.search) ||
                 (cr_id && cr_id.background) ||
                 !this._updateContent((cr_id && cr_id.offset) ? cr_id.offset : (r.rownum ? parseInt(r.rownum) - 1 : this.currentOffset()))) {
                 return;
@@ -581,9 +648,9 @@ var ViewPort = Class.create({
     // params = (object) [background, offset, rlist]
     _addRequest: function(view, r_id, params)
     {
-        var req_view = this.current_req.get(view), req;
+        var req_view = this.current_req[view], req;
         if (!req_view) {
-            req_view = this.current_req.set(view, $H());
+            req_view = this.current_req[view] = $H();
         }
 
         req = req_view.get(r_id);
@@ -596,7 +663,7 @@ var ViewPort = Class.create({
             }
         });
 
-        this.current_req_lookup.set(r_id, view);
+        this.current_req_lookup[r_id] = view;
     },
 
     // Removes a request to the current request queue.
@@ -604,14 +671,14 @@ var ViewPort = Class.create({
     // r_id = (string) The request ID to remove
     _removeRequest: function(view, r_id)
     {
-        var cr = this.current_req.get(view);
+        var cr = this.current_req[view];
         if (cr) {
             cr.unset(r_id);
             if (!cr.size()) {
-                this.current_req.unset(view);
+                delete this.current_req[view];
             }
         }
-        this.current_req_lookup.unset(r_id);
+        delete this.current_req_lookup[r_id];
     },
 
     // offset = (integer) TODO
@@ -644,9 +711,7 @@ var ViewPort = Class.create({
             c_nodes = rows.get('dataob');
             c.update(c_nodes.collect(this.prepareRow.bind(this)).join(''));
         } else {
-            // If loading a viewport for the first time, show a blank
-            // viewport rather than the empty viewport status message.
-            c.update((this.opts.empty && this.viewport_init != 1) ? this.opts.empty.innerHTML : '');
+            c.update(this.empty_msg);
         }
 
         if (this.opts.onContentComplete) {
@@ -660,20 +725,23 @@ var ViewPort = Class.create({
     {
         var r = Object.clone(row);
 
-        if (r.bg) {
-            r.bg = row.bg.clone();
-            if (this.getSelected().contains('uid', r.vp_id)) {
-                r.bg.push(this.opts.selected_class);
-            }
-            r.bg_string = r.bg.join(' ');
-        } else {
-            r.bg_string = '';
+        r.bg = r.bg
+            ? row.bg.clone()
+            : [];
+
+        if (this.getSelected().contains('uid', r.vp_id)) {
+            r.bg.push('vpRowSelected');
         }
 
+        r.bg.unshift('vpRow');
+
         if (this.opts.onContent) {
             this.opts.onContent(r);
         }
 
+        // Mandatory DOM ID and class information.
+        r.vpData = 'id="' + r.domid + '" class="' + r.bg.join(' ') + '"';
+
         return this.template.evaluate(r);
     },
 
@@ -700,8 +768,8 @@ var ViewPort = Class.create({
             this.opts.onFail();
         }
 
-        if (this.opts.error) {
-            this.opts.content.update(this.opts.error.innerHTML);
+        if (this.opts.errormsg) {
+            this.opts.content.update(new Element('SPAN', { className: 'vpError' }).insert(this.opts.errormsg));
         }
     },
 
@@ -747,13 +815,10 @@ var ViewPort = Class.create({
     _getBuffer: function(view, create)
     {
         view = view || this.view;
-        if (!create) {
-            var b = this.views.get(view);
-            if (b) {
-                return b.buffer;
-            }
-        }
-        return new ViewPort_Buffer(this, this.opts.buffer_pages, this.opts.limit_factor, view);
+
+        return (!create && this.views[view])
+            ? this.views[view]
+            : new ViewPort_Buffer(this, this.opts.buffer_pages, this.opts.limit_factor, view);
     },
 
     currentOffset: function()
@@ -766,7 +831,7 @@ var ViewPort = Class.create({
         if (!this.line_height) {
             // 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.content_class }).insert(new Element('DIV', { className: this.opts.row_class })).hide();
+            var d = new Element('DIV', { className: this.opts.content_class }).insert(new Element('DIV', { className: 'vpRow' })).hide();
             $(document.body).insert(d);
             this.line_height = d.getHeight();
             d.remove();
@@ -816,28 +881,27 @@ var ViewPort = Class.create({
             return this._renderViewport.bind(this, noupdate).defer();
         }
 
-        var diff, h, pane, setpane,
+        var diff, h, setpane,
             c = $(this.opts.content),
             de = document.documentElement,
             lh = this._getLineHeight();
 
         // Get split pane dimensions
         if (this.opts.split_pane) {
-            pane = $(this.opts.split_pane);
             if (this.show_split_pane) {
-                if (!pane.visible()) {
+                if (!this.opts.split_pane.visible()) {
                     this._initSplitBar();
-                    if (this.splitbar_loc &&
-                        this.splitbar_loc > 0) {
-                        this.splitbar_loc = this.page_size = Math.min(this.splitbar_loc, this.getPageSize('splitmax'));
+                    if (this.split_bar_loc &&
+                        this.split_bar_loc > 0) {
+                        this.split_bar_loc = this.page_size = Math.min(this.split_bar_loc, this.getPageSize('splitmax'));
                     } else {
                         this.page_size = this.getPageSize('default');
                     }
                 }
                 setpane = true;
-            } else if (pane.visible()) {
-                this.splitbar_loc = this.page_size;
-                $(pane, this.splitbar).invoke('hide');
+            } else if (this.opts.split_pane.visible()) {
+                this.split_bar_loc = this.page_size;
+                [ this.opts.split_pane, this.split_bar ].invoke('hide');
             }
         }
 
@@ -849,8 +913,8 @@ var ViewPort = Class.create({
         h = lh * this.page_size;
         c.setStyle({ height: h + 'px' });
         if (setpane) {
-            pane.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show();
-            this.splitbar.show();
+            this.opts.split_pane.setStyle({ height: (this._getMaxHeight() - h - lh) + 'px' }).show();
+            this.split_bar.show();
         } else if (diff = de.scrollHeight - de.clientHeight) {
             c.setStyle({ height: (lh * (this.page_size - 1)) + 'px' });
         }
@@ -862,12 +926,12 @@ var ViewPort = Class.create({
 
     _initSplitBar: function()
     {
-        if (this.splitbar) {
+        if (this.split_bar) {
             return;
         }
 
-        this.splitbar = $(this.opts.splitbar);
-        new Drag(this.splitbar, {
+        this.split_bar = $(this.opts.split_bar);
+        new Drag(this.split_bar, {
             constraint: 'vertical',
             ghosting: true,
             nodrop: true,
@@ -901,7 +965,7 @@ var ViewPort = Class.create({
                 }
             }.bind(this)
         });
-        this.splitbar.observe('dblclick', function() {
+        this.split_bar.observe('dblclick', function() {
             var old_size = this.page_size;
             this.page_size = this.getPageSize('default');
             this._renderViewport();
@@ -946,12 +1010,12 @@ var ViewPort = Class.create({
         if (!opts.add) {
             sel = this.getSelected();
             b.deselect(sel, true);
-            sel.get('div').invoke('removeClassName', this.opts.selected_class);
+            sel.get('div').invoke('removeClassName', 'vpRowSelected');
         }
         b.select(vs);
-        vs.get('div').invoke('addClassName', this.opts.selected_class);
-        if (this.opts.selectCallback) {
-            this.opts.selectCallback(vs, opts);
+        vs.get('div').invoke('addClassName', 'vpRowSelected');
+        if (this.opts.onSelect) {
+            this.opts.onSelect(vs, opts);
         }
     },
 
@@ -963,9 +1027,9 @@ var ViewPort = Class.create({
 
         if (vs.size() &&
             this._getBuffer().deselect(vs, opts && opts.clearall)) {
-            vs.get('div').invoke('removeClassName', this.opts.selected_class);
-            if (this.opts.deselectCallback) {
-                this.opts.deselectCallback(vs, opts)
+            vs.get('div').invoke('removeClassName', 'vpRowSelected');
+            if (this.opts.onDeselect) {
+                this.opts.onDeselect(vs, opts)
             }
         }
     },
@@ -981,6 +1045,7 @@ var ViewPort = Class.create({
  * ViewPort_Scroller
  */
 ViewPort_Scroller = Class.create({
+    // Variables initialized to undefined: noupdate
 
     initialize: function(vp)
     {
@@ -1062,15 +1127,7 @@ ViewPort_Scroller = Class.create({
     _onScroll: function()
     {
         if (!this.noupdate) {
-            if (this.vp.opts.onScroll) {
-                this.vp.opts.onScroll();
-            }
-
             this.vp.requestContentRefresh(this.currentOffset());
-
-            if (this.vp.opts.onScrollIdle) {
-                this.vp.opts.onScrollIdle();
-            }
         }
     },
 
@@ -1292,12 +1349,12 @@ ViewPort_Buffer = Class.create({
     clear: function()
     {
         this.data = $H();
-        this.partial = $H();
         this.mdata = $H({ total_rows: 0 });
-        this.usermdata = $H();
+        this.partial = $H();
         this.rowlist = $H();
         this.selected = new ViewPort_Selection(this);
         this.uidlist = $H();
+        this.usermdata = $H();
     },
 
     getMetaData: function(id)
@@ -1341,7 +1398,7 @@ ViewPort_Selection = Class.create({
     add: function(format, d)
     {
         var c = this._convert(format, d);
-        this.data = (this.data.size()) ? this.data.concat(c).uniq() : c;
+        this.data = this.data.size() ? this.data.concat(c).uniq() : c;
     },
 
     remove: function(format, d)
@@ -1352,6 +1409,7 @@ ViewPort_Selection = Class.create({
     _convert: function(format, d)
     {
         d = Object.isArray(d) ? d : [ d ];
+
         switch (format) {
         case 'dataob':
             return d.pluck('vp_id');
index f02c222..a2c74a0 100644 (file)
@@ -257,6 +257,8 @@ class DIMP
             'delete_folder' => _("Permanently delete %s?"),
             'hidealog' => _("Hide Alerts Log"),
             'noalerts' => _("No Alerts"),
+            'vp_empty' => _("There are no messages in this mailbox."),
+            'vp_error' => _("Could not get message list from server."),
         ));
 
         /* Gettext strings with individual escaping. */
index 1914440..4eab9b4 100644 (file)
@@ -58,8 +58,7 @@ class IMP_Views_ListMessages
 
         /* Create the base object. */
         $result = new stdClass;
-        $result->id = $mbox;
-        $result->reset = 0;
+        $result->view = $mbox;
         $result->totalrows = $msgcount;
         $result->label = $label;
         $result->cacheid = $imp_mailbox->getCacheID();
@@ -179,14 +178,14 @@ class IMP_Views_ListMessages
         /* Build the overview list for slice information. */
         if ($args['rangeslice']) {
             $slice = new stdClass;
-            $slice->id = $mbox;
+            $slice->view = $mbox;
             $slice->partial = 1;
             $slice->rowlist = $rowlist;
 
             $slice_data = array();
             foreach ($rowlist as $key => $val) {
                 $slice_data[$key] = array(
-                    'imapuid' => $sorted_list['s'][$val],
+                    'imapuid' => intval($sorted_list['s'][$val]),
                     'mailbox' => isset($sorted_list['m'][$val]['m']) ? $sorted_list['m'][$val]['m'] : $mbox,
                     'rownum' => $val
                 );
@@ -250,9 +249,7 @@ class IMP_Views_ListMessages
         while (list(,$ob) = each($overview['overview'])) {
             /* Initialize the header fields. */
             $msg = array(
-                'bg' => array('msgRow'),
-                'flag' => array(),
-                'imapuid' => $ob['uid'],
+                'imapuid' => intval($ob['uid']),
                 'menutype' => 'message',
                 'rownum' => $ob['seq'],
                 'view' => $ob['mailbox'],
@@ -271,8 +268,11 @@ class IMP_Views_ListMessages
                 'priority' => $ob['headers']->getValue('x-priority')
             ));
 
-            foreach ($flag_parse as $val) {
-                $msg['flag'][] = $val['flag'];
+            if (!empty($flag_parse)) {
+                $msg['flag'] = array();
+                foreach ($flag_parse as $val) {
+                    $msg['flag'][] = $val['flag'];
+                }
             }
 
             /* Specific flag checking. */
index d314edc..16a674a 100644 (file)
@@ -234,11 +234,7 @@ function _simpleButton($id, $text, $image, $imagedir = null)
     </div>
 
     <div id="msgSplitPane">
-     <div id="msgListDiv" class="noprint">
-      <div id="msgList_empty" style="display:none"><em><?php echo _("There are no messages in this mailbox.") ?></em></div>
-      <div id="msgList_error" style="display:none"><em><?php echo _("Could not get message list from server.") ?></em></div>
-      <div id="msgList" class="msglist">&nbsp;</div>
-     </div>
+     <div id="msgList" class="msglist noprint">&nbsp;</div>
      <div id="splitBar" style="display:none" class="splitBar noprint"></div>
      <div id="previewPane" style="display:none">
       <span id="msgLoading" class="loadingImg" style="display:none"></span>
index 660a66c..91f0f55 100644 (file)
@@ -2,6 +2,8 @@
  * mailbox-dimp.js - Template used to format the rows in the message list
  * display.
  *
+ * 'vpData' is a mandatory entry for ID/class information automatically
+ * generated by ViewPort.js.
  * 'status', 'style', and 'subjectdata' are entries that are internally
  * created by DimpBase.js.
  *
@@ -15,7 +17,7 @@
  */
 
 DimpBase.message_list_template =
-'<div id="#{domid}" class="#{bg_string}" style="#{style}">' +
+'<div #{vpData} style="#{style}">' +
  '<div class="msgStatus">' +
   '<div class="msCheck"></div>' +
   '#{status}' +
index b1db5e4..0d49cb3 100644 (file)
@@ -185,7 +185,7 @@ div.msgSize {
     border-bottom: 1px silver solid;
     float: left;
 }
-.msglist em {
+.msglist span.vpEmpty, .msglist span.vpError {
     font-weight: bold;
 }
 .msglistNoPreview {
@@ -193,7 +193,7 @@ div.msgSize {
 }
 
 /* Rows */
-.msglist div.msgRow, .msglist div.msgRow div {
+.msglist div.vpRow, .msglist div.vpRow div {
     cursor: pointer;
     font-size: 95%;
     white-space: nowrap;
@@ -202,10 +202,21 @@ div.msgSize {
     height: 16px;
     margin-bottom: 1px;
 }
-.msglist div.selectedRow {
+.msglist div.vpRowSelected {
     background: #ffc !important;
 }
 
+/* Rows: flags. */
+div.vpRow.flagLowpriority {
+    font-weight: normal !important;
+}
+div.vpRow.flagUnseen {
+    font-weight: bold;
+}
+div.vpRow.flagDeleted {
+    text-decoration: line-through;
+}
+
 /* Checkbox images */
 .msglist div.msCheck {
     background-image: url("graphics/checkbox_off.png");
@@ -213,7 +224,7 @@ div.msgSize {
 .msglist div.msCheck:hover {
     background-image: url("graphics/checkbox_over.png");
 }
-.msglist div.selectedRow div.msCheck {
+.msglist div.vpRowSelected div.msCheck {
     background-image: url("graphics/checkbox_on.png");
 }
 
@@ -974,7 +985,7 @@ span.dimpactionDrafts {
     height: 15px;
     width: 15px;
 }
-div.msgRow div.msgSubject span.flagUser {
+div.vpRow div.msgSubject span.flagUser {
     font-size: 90%;
     font-weight: normal;
     margin-right: 3px;
index df9cafb..a770637 100644 (file)
@@ -253,7 +253,7 @@ div.msgflags.flagPersonal {
 div.msgflags.flagHighpriority {
     background-image: url("graphics/mail_priority_high.png");
 }
-tr.flagLowpriority, div.msgRow.flagLowpriority {
+tr.flagLowpriority {
     font-weight: normal !important;
 }
 div.msgflags.flagLowpriority {
@@ -268,7 +268,7 @@ div.msgflags.flagEncryptmsg {
 div.msgflags.flagAttachmsg {
     background-image: url("graphics/attachment.png");
 }
-tr.flagUnseen, div.msgRow.flagUnseen {
+tr.flagUnseen {
     font-weight: bold;
 }
 div.msgflags.flagUnseen, span.contextImg.flagUnseen {
@@ -283,7 +283,7 @@ div.msgflags.flagDraft, span.contextImg.flagDraft {
 div.msgflags.flagFlagged, span.contextImg.flagFlagged {
     background-image: url("graphics/mail_flagged.png");
 }
-tr.flagDeleted *, div.msgRow.flagDeleted {
+tr.flagDeleted * {
     text-decoration: line-through;
 }
 div.msgflags.flagDeleted, span.contextImg.flagDeleted {