Bug 1171903 - Add endless scrolling to storage inspector to view more items. r=miker
☠☠ backed out by d429bacbed6c ☠ ☠
authorTim Nguyen <ntim.bugs@gmail.com>
Tue, 08 Sep 2015 10:38:03 +0530
changeset 261297 78a423ab972e6a7de5effb903f49ac7a056d60d8
parent 261296 ec99f1dfd66858d2d8226d3bad55d95b7f9349dc
child 261298 eb7ef800ed124c654c8096dd86ec300a12a1c2ce
push id64705
push usercbook@mozilla.com
push dateTue, 08 Sep 2015 14:02:43 +0000
treeherdermozilla-inbound@7fa38a962661 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1171903
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1171903 - Add endless scrolling to storage inspector to view more items. r=miker
browser/devtools/shared/widgets/TableWidget.js
browser/devtools/storage/ui.js
--- a/browser/devtools/shared/widgets/TableWidget.js
+++ b/browser/devtools/shared/widgets/TableWidget.js
@@ -1,29 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
+const EventEmitter = require("devtools/toolkit/event-emitter");
 
-const EventEmitter = require("devtools/toolkit/event-emitter");
+loader.lazyImporter(this, "setNamedTimeout",
+  "resource:///modules/devtools/ViewHelpers.jsm");
+loader.lazyImporter(this, "clearNamedTimeout",
+  "resource:///modules/devtools/ViewHelpers.jsm");
+
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-
+const AFTER_SCROLL_DELAY = 100;
 // Different types of events emitted by the Various components of the TableWidget
 const EVENTS = {
   TABLE_CLEARED: "table-cleared",
   COLUMN_SORTED: "column-sorted",
   COLUMN_TOGGLED: "column-toggled",
   ROW_SELECTED: "row-selected",
   ROW_UPDATED: "row-updated",
   HEADER_CONTEXT_MENU: "header-context-menu",
-  ROW_CONTEXT_MENU: "row-context-menu"
+  ROW_CONTEXT_MENU: "row-context-menu",
+  SCROLL_END: "scroll-end"
 };
 
 // Maximum number of character visible in any cell in the table. This is to avoid
 // making the cell take up all the space in a row.
 const MAX_VISIBLE_STRING_SIZE = 100;
 
 /**
  * A table widget with various features like resizble/toggleable columns,
@@ -57,16 +63,18 @@ function TableWidget(node, options={}) {
   this.highlightUpdated = highlightUpdated || false;
   this.removableColumns = removableColumns !== false;
 
   this.tbody = this.document.createElementNS(XUL_NS, "hbox");
   this.tbody.className = "table-widget-body theme-body";
   this.tbody.setAttribute("flex", "1");
   this.tbody.setAttribute("tabindex", "0");
   this._parent.appendChild(this.tbody);
+  this.afterScroll = this.afterScroll.bind(this);
+  this.tbody.addEventListener("scroll", this.onScroll.bind(this));
 
   this.placeholder = this.document.createElementNS(XUL_NS, "label");
   this.placeholder.className = "plain table-widget-empty-text";
   this.placeholder.setAttribute("flex", "1");
   this._parent.appendChild(this.placeholder);
 
   this.items = new Map();
   this.columns = new Map();
@@ -418,16 +426,37 @@ TableWidget.prototype = {
     }
 
     let sortedItems = this.columns.get(column).sort([...this.items.values()]);
     for (let [id, column] of this.columns) {
       if (id != column) {
         column.sort(sortedItems);
       }
     }
+  },
+
+  /**
+   * Calls the afterScroll function when the user has stopped scrolling
+   */
+  onScroll: function() {
+    clearNamedTimeout("table-scroll");
+    setNamedTimeout("table-scroll", AFTER_SCROLL_DELAY, this.afterScroll);
+  },
+
+  /**
+   * Emits the "scroll-end" event when the whole table is scrolled
+   */
+  afterScroll: function() {
+    let scrollHeight = this.tbody.getBoundingClientRect().height -
+        this.tbody.querySelector(".table-widget-column-header").clientHeight;
+
+    // Emit scroll-end event when 9/10 of the table is scrolled
+    if (this.tbody.scrollTop >= 0.9 * scrollHeight) {
+      this.emit("scroll-end");
+    }
   }
 };
 
 TableWidget.EVENTS = EVENTS;
 
 module.exports.TableWidget = TableWidget;
 
 /**
--- a/browser/devtools/storage/ui.js
+++ b/browser/devtools/storage/ui.js
@@ -67,16 +67,19 @@ let StorageUI = this.StorageUI = functio
   let tableNode = this._panelDoc.getElementById("storage-table");
   this.table = new TableWidget(tableNode, {
     emptyText: L10N.getStr("table.emptyText"),
     highlightUpdated: true,
   });
   this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
   this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
 
+  this.handleScrollEnd = this.handleScrollEnd.bind(this);
+  this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
+
   this.sidebar = this._panelDoc.getElementById("storage-sidebar");
   this.sidebar.setAttribute("width", "300");
   this.view = new VariablesView(this.sidebar.firstChild,
                                 GENERIC_VARIABLES_VIEW_SETTINGS);
 
   this.front.listStores().then(storageTypes => {
     this.populateStorageTree(storageTypes);
   }).then(null, console.error);
@@ -94,16 +97,17 @@ let StorageUI = this.StorageUI = functio
 };
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
   shouldResetColumns: true,
+  shouldLoadMoreItems: true,
 
   destroy: function() {
     this.front.off("stores-update", this.onUpdate);
     this.front.off("stores-cleared", this.onCleared);
     this._panelDoc.removeEventListener("keypress", this.handleKeypress);
     this._telemetry.toolClosed("storage");
   },
 
@@ -297,21 +301,24 @@ StorageUI.prototype = {
    *
    * @param {string} type
    *        The type of storage. Ex. "cookies"
    * @param {string} host
    *        Hostname
    * @param {array} names
    *        Names of particular store objects. Empty if all are requested
    * @param {number} reason
-   *        2 for update, 1 for new row in an existing table and 0 when
-   *        populating a table for the first time for the given host/type
+   *        3 for loading next 50 items, 2 for update, 1 for new row in an
+   *        existing table and 0 when populating a table for the first time
+   *        for the given host/type
    */
   fetchStorageObjects: function(type, host, names, reason) {
-    this.storageTypes[type].getStoreObjects(host, names).then(({data}) => {
+    let fetchOpts = reason === 3 ? {offset: this.itemOffset}
+                                 : {};
+    this.storageTypes[type].getStoreObjects(host, names, fetchOpts).then(({data}) => {
       if (!data.length) {
         this.emit("store-objects-updated");
         return;
       }
       if (this.shouldResetColumns) {
         this.resetColumns(data[0], type);
       }
       this.populateTable(data, reason);
@@ -532,16 +539,17 @@ StorageUI.prototype = {
     if (!host) {
       return;
     }
     if (item.length > 2) {
       names = [JSON.stringify(item.slice(2))];
     }
     this.shouldResetColumns = true;
     this.fetchStorageObjects(type, host, names, 0);
+    this.itemOffset = 0;
   },
 
   /**
    * Resets the column headers in the storage table with the pased object `data`
    *
    * @param {object} data
    *        The object from which key and values will be used for naming the
    *        headers of the columns
@@ -564,19 +572,19 @@ StorageUI.prototype = {
   },
 
   /**
    * Populates or updates the rows in the storage table.
    *
    * @param {array[object]} data
    *        Array of objects to be populated in the storage table
    * @param {number} reason
-   *        The reason of this populateTable call. 2 for update, 1 for new row
-   *        in an existing table and 0 when populating a table for the first
-   *        time for the given host/type
+   *        The reason of this populateTable call. 3 for loading next 50 items,
+   *        2 for update, 1 for new row in an existing table and 0 when 
+   *        populating a table for the first time for the given host/type
    */
   populateTable: function(data, reason) {
     for (let item of data) {
       if (item.value) {
         item.valueActor = item.value;
         item.value = item.value.initial || "";
       }
       if (item.expires != null) {
@@ -585,34 +593,52 @@ StorageUI.prototype = {
           : L10N.getStr("label.expires.session");
       }
       if (item.creationTime != null) {
         item.creationTime = new Date(item.creationTime).toLocaleString();
       }
       if (item.lastAccessed != null) {
         item.lastAccessed = new Date(item.lastAccessed).toLocaleString();
       }
-      if (reason < 2) {
+      if (reason < 2 || reason == 3) {
         this.table.push(item, reason == 0);
       } else {
         this.table.update(item);
         if (item == this.table.selectedRow && !this.sidebar.hidden) {
           this.displayObjectSidebar();
         }
       }
+      this.shouldLoadMoreItems = true;
     }
   },
 
   /**
    * Handles keypress event on the body table to close the sidebar when open
    *
    * @param {DOMEvent} event
    *        The event passed by the keypress event.
    */
   handleKeypress: function(event) {
     if (event.keyCode == event.DOM_VK_ESCAPE && !this.sidebar.hidden) {
       // Stop Propagation to prevent opening up of split console
       this.hideSidebar();
       event.stopPropagation();
       event.preventDefault();
     }
+  },
+
+  /**
+   * Handles endless scrolling for the table
+   */
+  handleScrollEnd: function() {
+    if (!this.shouldLoadMoreItems) return;
+    this.shouldLoadMoreItems = false;
+    this.itemOffset += 50;
+
+    let item = this.tree.selectedItem;
+    let [type, host, db, objectStore] = item;
+    let names = null;
+    if (item.length > 2) {
+      names = [JSON.stringify(item.slice(2))];
+    }
+    this.fetchStorageObjects(type, host, names, 3);
   }
 };