Bug 455553 - Part 1 - XUL/HTML Page and Scripts; r=blair,dietrich
☠☠ backed out by d1cdc35292f1 ☠ ☠
authorTim Taubert <tim.taubert@gmx.de>
Wed, 25 Jan 2012 23:40:18 +0100
changeset 86608 34157f4059bac86d59c587d861e3e2173fd062ad
parent 86606 8985a8e405feca43d935333d879a005908051e37
child 86609 3f30da5d0bc30bfd7142b9a3c8b7145f24d2d16b
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblair, dietrich
bugs455553
milestone12.0a1
Bug 455553 - Part 1 - XUL/HTML Page and Scripts; r=blair,dietrich
browser/base/content/newtab/batch.js
browser/base/content/newtab/cells.js
browser/base/content/newtab/drag.js
browser/base/content/newtab/drop.js
browser/base/content/newtab/dropPreview.js
browser/base/content/newtab/dropTargetShim.js
browser/base/content/newtab/grid.js
browser/base/content/newtab/newTab.js
browser/base/content/newtab/newTab.xul
browser/base/content/newtab/page.js
browser/base/content/newtab/sites.js
browser/base/content/newtab/toolbar.js
browser/base/content/newtab/transformations.js
browser/base/content/newtab/updater.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/batch.js
@@ -0,0 +1,76 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This class makes it easy to wait until a batch of callbacks has finished.
+ *
+ * Example:
+ *
+ * let batch = new Batch(function () alert("finished"));
+ * let pop = batch.pop.bind(batch);
+ *
+ * for (let i = 0; i < 5; i++) {
+ *   batch.push();
+ *   setTimeout(pop, i * 1000);
+ * }
+ *
+ * batch.close();
+ */
+function Batch(aCallback) {
+  this._callback = aCallback;
+}
+
+Batch.prototype = {
+  /**
+   * The number of batch entries.
+   */
+  _count: 0,
+
+  /**
+   * Whether this batch is closed.
+   */
+  _closed: false,
+
+  /**
+   * Increases the number of batch entries by one.
+   */
+  push: function Batch_push() {
+    if (!this._closed)
+      this._count++;
+  },
+
+  /**
+   * Decreases the number of batch entries by one.
+   */
+  pop: function Batch_pop() {
+    if (this._count)
+      this._count--;
+
+    if (this._closed)
+      this._check();
+  },
+
+  /**
+   * Closes the batch so that no new entries can be added.
+   */
+  close: function Batch_close() {
+    if (this._closed)
+      return;
+
+    this._closed = true;
+    this._check();
+  },
+
+  /**
+   * Checks if the batch has finished.
+   */
+  _check: function Batch_check() {
+    if (this._count == 0 && this._callback) {
+      this._callback();
+      this._callback = null;
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/cells.js
@@ -0,0 +1,137 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This class manages a cell's DOM node (not the actually cell content, a site).
+ * It's mostly read-only, i.e. all manipulation of both position and content
+ * aren't handled here.
+ */
+function Cell(aGrid, aNode) {
+  this._grid = aGrid;
+  this._node = aNode;
+  this._node._newtabCell = this;
+
+  // Register drag-and-drop event handlers.
+  ["DragEnter", "DragOver", "DragExit", "Drop"].forEach(function (aType) {
+    let method = "on" + aType;
+    this[method] = this[method].bind(this);
+    this._node.addEventListener(aType.toLowerCase(), this[method], false);
+  }, this);
+}
+
+Cell.prototype = {
+  /**
+   *
+   */
+  _grid: null,
+
+  /**
+   * The cell's DOM node.
+   */
+  get node() this._node,
+
+  /**
+   * The cell's offset in the grid.
+   */
+  get index() {
+    let index = this._grid.cells.indexOf(this);
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "index", {value: index, enumerable: true});
+
+    return index;
+  },
+
+  /**
+   * The previous cell in the grid.
+   */
+  get previousSibling() {
+    let prev = this.node.previousElementSibling;
+    prev = prev && prev._newtabCell;
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "previousSibling", {value: prev, enumerable: true});
+
+    return prev;
+  },
+
+  /**
+   * The next cell in the grid.
+   */
+  get nextSibling() {
+    let next = this.node.nextElementSibling;
+    next = next && next._newtabCell;
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "nextSibling", {value: next, enumerable: true});
+
+    return next;
+  },
+
+  /**
+   * The site contained in the cell, if any.
+   */
+  get site() {
+    let firstChild = this.node.firstElementChild;
+    return firstChild && firstChild._newtabSite;
+  },
+
+  /**
+   * Checks whether the cell contains a pinned site.
+   * @return Whether the cell contains a pinned site.
+   */
+  containsPinnedSite: function Cell_containsPinnedSite() {
+    let site = this.site;
+    return site && site.isPinned();
+  },
+
+  /**
+   * Checks whether the cell contains a site (is empty).
+   * @return Whether the cell is empty.
+   */
+  isEmpty: function Cell_isEmpty() {
+    return !this.site;
+  },
+
+  /**
+   * Event handler for the 'dragenter' event.
+   * @param aEvent The dragenter event.
+   */
+  onDragEnter: function Cell_onDragEnter(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      gDrop.enter(this, aEvent);
+    }
+  },
+
+  /**
+   * Event handler for the 'dragover' event.
+   * @param aEvent The dragover event.
+   */
+  onDragOver: function Cell_onDragOver(aEvent) {
+    if (gDrag.isValid(aEvent))
+      aEvent.preventDefault();
+  },
+
+  /**
+   * Event handler for the 'dragexit' event.
+   * @param aEvent The dragexit event.
+   */
+  onDragExit: function Cell_onDragExit(aEvent) {
+    gDrop.exit(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'drop' event.
+   * @param aEvent The drop event.
+   */
+  onDrop: function Cell_onDrop(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      gDrop.drop(this, aEvent);
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/drag.js
@@ -0,0 +1,140 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton implements site dragging functionality.
+ */
+let gDrag = {
+  /**
+   * The site offset to the drag start point.
+   */
+  _offsetX: null,
+  _offsetY: null,
+
+  /**
+   * The site that is dragged.
+   */
+  _draggedSite: null,
+  get draggedSite() this._draggedSite,
+
+  /**
+   * The cell width/height at the point the drag started.
+   */
+  _cellWidth: null,
+  _cellHeight: null,
+  get cellWidth() this._cellWidth,
+  get cellHeight() this._cellHeight,
+
+  /**
+   * Start a new drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragstart' event.
+   */
+  start: function Drag_start(aSite, aEvent) {
+    this._draggedSite = aSite;
+
+    // Prevent moz-transform for left, top.
+    aSite.node.setAttribute("dragged", "true");
+
+    // Make sure the dragged site is floating above the grid.
+    aSite.node.setAttribute("ontop", "true");
+
+    this._setDragData(aSite, aEvent);
+
+    // Store the cursor offset.
+    let node = aSite.node;
+    let rect = node.getBoundingClientRect();
+    this._offsetX = aEvent.clientX - rect.left;
+    this._offsetY = aEvent.clientY - rect.top;
+
+    // Store the cell dimensions.
+    let cellNode = aSite.cell.node;
+    this._cellWidth = cellNode.offsetWidth;
+    this._cellHeight = cellNode.offsetHeight;
+
+    gTransformation.freezeSitePosition(aSite);
+  },
+
+  /**
+   * Handles the 'drag' event.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'drag' event.
+   */
+  drag: function Drag_drag(aSite, aEvent) {
+    // Get the viewport size.
+    let {clientWidth, clientHeight} = document.documentElement;
+
+    // We'll want a padding of 5px.
+    let border = 5;
+
+    // Enforce minimum constraints to keep the drag image inside the window.
+    let left = Math.max(scrollX + aEvent.clientX - this._offsetX, border);
+    let top = Math.max(scrollY + aEvent.clientY - this._offsetY, border);
+
+    // Enforce maximum constraints to keep the drag image inside the window.
+    left = Math.min(left, scrollX + clientWidth - this.cellWidth - border);
+    top = Math.min(top, scrollY + clientHeight - this.cellHeight - border);
+
+    // Update the drag image's position.
+    gTransformation.setSitePosition(aSite, {left: left, top: top});
+  },
+
+  /**
+   * Ends the current drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragend' event.
+   */
+  end: function Drag_end(aSite, aEvent) {
+    aSite.node.removeAttribute("dragged");
+
+    // Slide the dragged site back into its cell (may be the old or the new cell).
+    gTransformation.slideSiteTo(aSite, aSite.cell, {
+      unfreeze: true,
+      callback: function () aSite.node.removeAttribute("ontop")
+    });
+
+    this._draggedSite = null;
+  },
+
+  /**
+   * Checks whether we're responsible for a given drag event.
+   * @param aEvent The drag event to check.
+   * @return Whether we should handle this drag and drop operation.
+   */
+  isValid: function Drag_isValid(aEvent) {
+    let dt = aEvent.dataTransfer;
+    return dt && dt.types.contains("text/x-moz-url");
+  },
+
+  /**
+   * Initializes the drag data for the current drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragstart' event.
+   */
+  _setDragData: function Drag_setDragData(aSite, aEvent) {
+    let {url, title} = aSite;
+
+    let dt = aEvent.dataTransfer;
+    dt.mozCursor = "default";
+    dt.effectAllowed = "move";
+    dt.setData("text/plain", url);
+    dt.setData("text/uri-list", url);
+    dt.setData("text/x-moz-url", url + "\n" + title);
+    dt.setData("text/html", "<a href=\"" + url + "\">" + url + "</a>");
+
+    // Create and use an empty drag element. We don't want to use the default
+    // drag image with its default opacity.
+    let dragElement = document.createElementNS(HTML_NAMESPACE, "div");
+    dragElement.classList.add("drag-element");
+    let body = document.getElementById("body");
+    body.appendChild(dragElement);
+    dt.setDragImage(dragElement, 0, 0);
+
+    // After the 'dragstart' event has been processed we can remove the
+    // temporary drag element from the DOM.
+    setTimeout(function () body.removeChild(dragElement), 0);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/drop.js
@@ -0,0 +1,147 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+// A little delay that prevents the grid from being too sensitive when dragging
+// sites around.
+const DELAY_REARRANGE_MS = 100;
+
+/**
+ * This singleton implements site dropping functionality.
+ */
+let gDrop = {
+  /**
+   * The last drop target.
+   */
+  _lastDropTarget: null,
+
+  /**
+   * Handles the 'dragenter' event.
+   * @param aCell The drop target cell.
+   */
+  enter: function Drop_enter(aCell) {
+    this._delayedRearrange(aCell);
+  },
+
+  /**
+   * Handles the 'dragexit' event.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   */
+  exit: function Drop_exit(aCell, aEvent) {
+    if (aEvent.dataTransfer && !aEvent.dataTransfer.mozUserCancelled) {
+      this._delayedRearrange();
+    } else {
+      // The drag operation has been cancelled.
+      this._cancelDelayedArrange();
+      this._rearrange();
+    }
+  },
+
+  /**
+   * Handles the 'drop' event.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   * @param aCallback The callback to call when the drop is finished.
+   */
+  drop: function Drop_drop(aCell, aEvent, aCallback) {
+    // The cell that is the drop target could contain a pinned site. We need
+    // to find out where that site has gone and re-pin it there.
+    if (aCell.containsPinnedSite())
+      this._repinSitesAfterDrop(aCell);
+
+    // Pin the dragged or insert the new site.
+    this._pinDraggedSite(aCell, aEvent);
+
+    this._cancelDelayedArrange();
+
+    // Update the grid and move all sites to their new places.
+    gUpdater.updateGrid(aCallback);
+  },
+
+  /**
+   * Re-pins all pinned sites in their (new) positions.
+   * @param aCell The drop target cell.
+   */
+  _repinSitesAfterDrop: function Drop_repinSitesAfterDrop(aCell) {
+    let sites = gDropPreview.rearrange(aCell);
+
+    // Filter out pinned sites.
+    let pinnedSites = sites.filter(function (aSite) {
+      return aSite && aSite.isPinned();
+    });
+
+    // Re-pin all shifted pinned cells.
+    pinnedSites.forEach(function (aSite) aSite.pin(sites.indexOf(aSite)), this);
+  },
+
+  /**
+   * Pins the dragged site in its new place.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   */
+  _pinDraggedSite: function Drop_pinDraggedSite(aCell, aEvent) {
+    let index = aCell.index;
+    let draggedSite = gDrag.draggedSite;
+
+    if (draggedSite) {
+      // Pin the dragged site at its new place.
+      if (aCell != draggedSite.cell)
+        draggedSite.pin(index);
+    } else {
+      // A new link was dragged onto the grid. Create it by pinning its URL.
+      let dt = aEvent.dataTransfer;
+      let [url, title] = dt.getData("text/x-moz-url").split(/[\r\n]+/);
+      gPinnedLinks.pin({url: url, title: title}, index);
+    }
+  },
+
+  /**
+   * Time a rearrange with a little delay.
+   * @param aCell The drop target cell.
+   */
+  _delayedRearrange: function Drop_delayedRearrange(aCell) {
+    // The last drop target didn't change so there's no need to re-arrange.
+    if (this._lastDropTarget == aCell)
+      return;
+
+    let self = this;
+
+    function callback() {
+      self._rearrangeTimeout = null;
+      self._rearrange(aCell);
+    }
+
+    this._cancelDelayedArrange();
+    this._rearrangeTimeout = setTimeout(callback, DELAY_REARRANGE_MS);
+
+    // Store the last drop target.
+    this._lastDropTarget = aCell;
+  },
+
+  /**
+   * Cancels a timed rearrange, if any.
+   */
+  _cancelDelayedArrange: function Drop_cancelDelayedArrange() {
+    if (this._rearrangeTimeout) {
+      clearTimeout(this._rearrangeTimeout);
+      this._rearrangeTimeout = null;
+    }
+  },
+
+  /**
+   * Rearrange all sites in the grid depending on the current drop target.
+   * @param aCell The drop target cell.
+   */
+  _rearrange: function Drop_rearrange(aCell) {
+    let sites = gGrid.sites;
+
+    // We need to rearrange the grid only if there's a current drop target.
+    if (aCell)
+      sites = gDropPreview.rearrange(aCell);
+
+    gTransformation.rearrangeSites(sites, {unfreeze: !aCell});
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/dropPreview.js
@@ -0,0 +1,222 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton provides the ability to re-arrange the current grid to
+ * indicate the transformation that results from dropping a cell at a certain
+ * position.
+ */
+let gDropPreview = {
+  /**
+   * Rearranges the sites currently contained in the grid when a site would be
+   * dropped onto the given cell.
+   * @param aCell The drop target cell.
+   * @return The re-arranged array of sites.
+   */
+  rearrange: function DropPreview_rearrange(aCell) {
+    let sites = gGrid.sites;
+
+    // Insert the dragged site into the current grid.
+    this._insertDraggedSite(sites, aCell);
+
+    // After the new site has been inserted we need to correct the positions
+    // of all pinned tabs that have been moved around.
+    this._repositionPinnedSites(sites, aCell);
+
+    return sites;
+  },
+
+  /**
+   * Inserts the currently dragged site into the given array of sites.
+   * @param aSites The array of sites to insert into.
+   * @param aCell The drop target cell.
+   */
+  _insertDraggedSite: function DropPreview_insertDraggedSite(aSites, aCell) {
+    let dropIndex = aCell.index;
+    let draggedSite = gDrag.draggedSite;
+
+    // We're currently dragging a site.
+    if (draggedSite) {
+      let dragCell = draggedSite.cell;
+      let dragIndex = dragCell.index;
+
+      // Move the dragged site into its new position.
+      if (dragIndex != dropIndex) {
+        aSites.splice(dragIndex, 1);
+        aSites.splice(dropIndex, 0, draggedSite);
+      }
+    // We're handling an external drag item.
+    } else {
+      aSites.splice(dropIndex, 0, null);
+    }
+  },
+
+  /**
+   * Correct the position of all pinned sites that might have been moved to
+   * different positions after the dragged site has been inserted.
+   * @param aSites The array of sites containing the dragged site.
+   * @param aCell The drop target cell.
+   */
+  _repositionPinnedSites:
+    function DropPreview_repositionPinnedSites(aSites, aCell) {
+
+    // Collect all pinned sites.
+    let pinnedSites = this._filterPinnedSites(aSites, aCell);
+
+    // Correct pinned site positions.
+    pinnedSites.forEach(function (aSite) {
+      aSites[aSites.indexOf(aSite)] = aSites[aSite.cell.index];
+      aSites[aSite.cell.index] = aSite;
+    }, this);
+
+    // There might be a pinned cell that got pushed out of the grid, try to
+    // sneak it in by removing a lower-priority cell.
+    if (this._hasOverflowedPinnedSite(aSites, aCell))
+      this._repositionOverflowedPinnedSite(aSites, aCell);
+  },
+
+  /**
+   * Filter pinned sites out of the grid that are still on their old positions
+   * and have not moved.
+   * @param aSites The array of sites to filter.
+   * @param aCell The drop target cell.
+   * @return The filtered array of sites.
+   */
+  _filterPinnedSites: function DropPreview_filterPinnedSites(aSites, aCell) {
+    let draggedSite = gDrag.draggedSite;
+
+    // When dropping on a cell that contains a pinned site make sure that all
+    // pinned cells surrounding the drop target are moved as well.
+    let range = this._getPinnedRange(aCell);
+
+    return aSites.filter(function (aSite, aIndex) {
+      // The site must be valid, pinned and not the dragged site.
+      if (!aSite || aSite == draggedSite || !aSite.isPinned())
+        return false;
+
+      let index = aSite.cell.index;
+
+      // If it's not in the 'pinned range' it's a valid pinned site.
+      return (index > range.end || index < range.start);
+    });
+  },
+
+  /**
+   * Determines the range of pinned sites surrounding the drop target cell.
+   * @param aCell The drop target cell.
+   * @return The range of pinned cells.
+   */
+  _getPinnedRange: function DropPreview_getPinnedRange(aCell) {
+    let dropIndex = aCell.index;
+    let range = {start: dropIndex, end: dropIndex};
+
+    // We need a pinned range only when dropping on a pinned site.
+    if (aCell.containsPinnedSite()) {
+      let links = gPinnedLinks.links;
+
+      // Find all previous siblings of the drop target that are pinned as well.
+      while (range.start && links[range.start - 1])
+        range.start--;
+
+      let maxEnd = links.length - 1;
+
+      // Find all next siblings of the drop target that are pinned as well.
+      while (range.end < maxEnd && links[range.end + 1])
+        range.end++;
+    }
+
+    return range;
+  },
+
+  /**
+   * Checks if the given array of sites contains a pinned site that has
+   * been pushed out of the grid.
+   * @param aSites The array of sites to check.
+   * @param aCell The drop target cell.
+   * @return Whether there is an overflowed pinned cell.
+   */
+  _hasOverflowedPinnedSite:
+    function DropPreview_hasOverflowedPinnedSite(aSites, aCell) {
+
+    // If the drop target isn't pinned there's no way a pinned site has been
+    // pushed out of the grid so we can just exit here.
+    if (!aCell.containsPinnedSite())
+      return false;
+
+    let cells = gGrid.cells;
+
+    // No cells have been pushed out of the grid, nothing to do here.
+    if (aSites.length <= cells.length)
+      return false;
+
+    let overflowedSite = aSites[cells.length];
+
+    // Nothing to do if the site that got pushed out of the grid is not pinned.
+    return (overflowedSite && overflowedSite.isPinned());
+  },
+
+  /**
+   * We have a overflowed pinned site that we need to re-position so that it's
+   * visible again. We try to find a lower-priority cell (empty or containing
+   * an unpinned site) that we can move it to.
+   * @param aSites The array of sites.
+   * @param aCell The drop target cell.
+   */
+  _repositionOverflowedPinnedSite:
+    function DropPreview_repositionOverflowedPinnedSite(aSites, aCell) {
+
+    // Try to find a lower-priority cell (empty or containing an unpinned site).
+    let index = this._indexOfLowerPrioritySite(aSites, aCell);
+
+    if (index > -1) {
+      let cells = gGrid.cells;
+      let dropIndex = aCell.index;
+
+      // Move all pinned cells to their new positions to let the overflowed
+      // site fit into the grid.
+      for (let i = index + 1, lastPosition = index; i < aSites.length; i++) {
+        if (i != dropIndex) {
+          aSites[lastPosition] = aSites[i];
+          lastPosition = i;
+        }
+      }
+
+      // Finally, remove the overflowed site from its previous position.
+      aSites.splice(cells.length, 1);
+    }
+  },
+
+  /**
+   * Finds the index of the last cell that is empty or contains an unpinned
+   * site. These are considered to be of a lower priority.
+   * @param aSites The array of sites.
+   * @param aCell The drop target cell.
+   * @return The cell's index.
+   */
+  _indexOfLowerPrioritySite:
+    function DropPreview_indexOfLowerPrioritySite(aSites, aCell) {
+
+    let cells = gGrid.cells;
+    let dropIndex = aCell.index;
+
+    // Search (beginning with the last site in the grid) for a site that is
+    // empty or unpinned (an thus lower-priority) and can be pushed out of the
+    // grid instead of the pinned site.
+    for (let i = cells.length - 1; i >= 0; i--) {
+      // The cell that is our drop target is not a good choice.
+      if (i == dropIndex)
+        continue;
+
+      let site = aSites[i];
+
+      // We can use the cell only if it's empty or the site is un-pinned.
+      if (!site || !site.isPinned())
+        return i;
+    }
+
+    return -1;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/dropTargetShim.js
@@ -0,0 +1,178 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton provides a custom drop target detection. We need this because
+ * the default DnD target detection relies on the cursor's position. We want
+ * to pick a drop target based on the dragged site's position.
+ */
+let gDropTargetShim = {
+  /**
+   * Cache for the position of all cells, cleaned after drag finished.
+   */
+  _cellPositions: null,
+
+  /**
+   * The last drop target that was hovered.
+   */
+  _lastDropTarget: null,
+
+  /**
+   * Initializes the drop target shim.
+   */
+  init: function DropTargetShim_init() {
+    let node = gGrid.node;
+
+    this._dragover = this._dragover.bind(this);
+
+    // Add drag event handlers.
+    node.addEventListener("dragstart", this._start.bind(this), true);
+    // XXX bug 505521 - Don't listen for drag, it's useless at the moment.
+    //node.addEventListener("drag", this._drag.bind(this), false);
+    node.addEventListener("dragend", this._end.bind(this), true);
+  },
+
+  /**
+   * Handles the 'dragstart' event.
+   * @param aEvent The 'dragstart' event.
+   */
+  _start: function DropTargetShim_start(aEvent) {
+    gGrid.lock();
+
+    // XXX bug 505521 - Listen for dragover on the document.
+    document.documentElement.addEventListener("dragover", this._dragover, false);
+  },
+
+  /**
+   * Handles the 'drag' event and determines the current drop target.
+   * @param aEvent The 'drag' event.
+   */
+  _drag: function DropTargetShim_drag(aEvent) {
+    // Let's see if we find a drop target.
+    let target = this._findDropTarget(aEvent);
+
+    if (target == this._lastDropTarget) {
+      // XXX bug 505521 - Don't fire dragover for now (causes recursion).
+      /*if (target)
+        // The last drop target is valid and didn't change.
+        this._dispatchEvent(aEvent, "dragover", target);*/
+    } else {
+      if (this._lastDropTarget)
+        // We left the last drop target.
+        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+
+      if (target)
+        // We're now hovering a (new) drop target.
+        this._dispatchEvent(aEvent, "dragenter", target);
+
+      if (this._lastDropTarget)
+        // We left the last drop target.
+        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+
+      this._lastDropTarget = target;
+    }
+  },
+
+  /**
+   * Handles the 'dragover' event as long as bug 505521 isn't fixed to get
+   * current mouse cursor coordinates while dragging.
+   * @param aEvent The 'dragover' event.
+   */
+  _dragover: function DropTargetShim_dragover(aEvent) {
+    let sourceNode = aEvent.dataTransfer.mozSourceNode;
+    gDrag.drag(sourceNode._newtabSite, aEvent);
+
+    this._drag(aEvent);
+  },
+
+  /**
+   * Handles the 'dragend' event.
+   * @param aEvent The 'dragend' event.
+   */
+  _end: function DropTargetShim_end(aEvent) {
+    // Make sure to determine the current drop target in case the dragenter
+    // event hasn't been fired.
+    this._drag(aEvent);
+
+    if (this._lastDropTarget) {
+      if (aEvent.dataTransfer.mozUserCancelled) {
+        // The drag operation was cancelled.
+        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+      } else {
+        // A site was successfully dropped.
+        this._dispatchEvent(aEvent, "drop", this._lastDropTarget);
+      }
+
+      // Clean up.
+      this._lastDropTarget = null;
+      this._cellPositions = null;
+    }
+
+    gGrid.unlock();
+
+    // XXX bug 505521 - Remove the document's dragover listener.
+    document.documentElement.removeEventListener("dragover", this._dragover, false);
+  },
+
+  /**
+   * Determines the current drop target by matching the dragged site's position
+   * against all cells in the grid.
+   * @return The currently hovered drop target or null.
+   */
+  _findDropTarget: function DropTargetShim_findDropTarget() {
+    // These are the minimum intersection values - we want to use the cell if
+    // the site is >= 50% hovering its position.
+    let minWidth = gDrag.cellWidth / 2;
+    let minHeight = gDrag.cellHeight / 2;
+
+    let cellPositions = this._getCellPositions();
+    let rect = gTransformation.getNodePosition(gDrag.draggedSite.node);
+
+    // Compare each cell's position to the dragged site's position.
+    for (let i = 0; i < cellPositions.length; i++) {
+      let inter = rect.intersect(cellPositions[i].rect);
+
+      // If the intersection is big enough we found a drop target.
+      if (inter.width >= minWidth && inter.height >= minHeight)
+        return cellPositions[i].cell;
+    }
+
+    // No drop target found.
+    return null;
+  },
+
+  /**
+   * Gets the positions of all cell nodes.
+   * @return The (cached) cell positions.
+   */
+  _getCellPositions: function DropTargetShim_getCellPositions() {
+    if (this._cellPositions)
+      return this._cellPositions;
+
+    return this._cellPositions = gGrid.cells.map(function (cell) {
+      return {cell: cell, rect: gTransformation.getNodePosition(cell.node)};
+    });
+  },
+
+  /**
+   * Dispatches a custom DragEvent on the given target node.
+   * @param aEvent The source event.
+   * @param aType The event type.
+   * @param aTarget The target node that receives the event.
+   */
+  _dispatchEvent:
+    function DropTargetShim_dispatchEvent(aEvent, aType, aTarget) {
+
+    let node = aTarget.node;
+    let event = document.createEvent("DragEvents");
+
+    event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false,
+                        false, false, 0, node, aEvent.dataTransfer);
+
+    node.dispatchEvent(event);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/grid.js
@@ -0,0 +1,132 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton represents the grid that contains all sites.
+ */
+let gGrid = {
+  /**
+   * The DOM node of the grid.
+   */
+  _node: null,
+  get node() this._node,
+
+  /**
+   * The cached DOM fragment for sites.
+   */
+  _siteFragment: null,
+
+  /**
+   * All cells contained in the grid.
+   */
+  get cells() {
+    let children = this.node.querySelectorAll("li");
+    let cells = [new Cell(this, child) for each (child in children)];
+
+    // Replace the getter with our cached value.
+    Object.defineProperty(this, "cells", {value: cells, enumerable: true});
+
+    return cells;
+  },
+
+  /**
+   * All sites contained in the grid's cells. Sites may be empty.
+   */
+  get sites() [cell.site for each (cell in this.cells)],
+
+  /**
+   * Initializes the grid.
+   * @param aSelector The query selector of the grid.
+   */
+  init: function Grid_init(aSelector) {
+    this._node = document.querySelector(aSelector);
+    this._createSiteFragment();
+    this._draw();
+  },
+
+  /**
+   * Creates a new site in the grid.
+   * @param aLink The new site's link.
+   * @param aCell The cell that will contain the new site.
+   * @return The newly created site.
+   */
+  createSite: function Grid_createSite(aLink, aCell) {
+    let node = aCell.node;
+    node.appendChild(this._siteFragment.cloneNode(true));
+    return new Site(node.firstElementChild, aLink);
+  },
+
+  /**
+   * Refreshes the grid and re-creates all sites.
+   */
+  refresh: function Grid_refresh() {
+    // Remove all sites.
+    this.cells.forEach(function (cell) {
+      let node = cell.node;
+      let child = node.firstElementChild;
+
+      if (child)
+        node.removeChild(child);
+    }, this);
+
+    // Draw the grid again.
+    this._draw();
+  },
+
+  /**
+   * Locks the grid to block all pointer events.
+   */
+  lock: function Grid_lock() {
+    this.node.setAttribute("locked", "true");
+  },
+
+  /**
+   * Unlocks the grid to allow all pointer events.
+   */
+  unlock: function Grid_unlock() {
+    this.node.removeAttribute("locked");
+  },
+
+  /**
+   * Creates the DOM fragment that is re-used when creating sites.
+   */
+  _createSiteFragment: function Grid_createSiteFragment() {
+    let site = document.createElementNS(HTML_NAMESPACE, "a");
+    site.classList.add("site");
+    site.setAttribute("draggable", "true");
+
+    // Create the site's inner HTML code.
+    site.innerHTML =
+      '<img class="site-img" width="' + THUMB_WIDTH +'" ' +
+      ' height="' + THUMB_HEIGHT + '" alt=""/>' +
+      '<span class="site-title"/>' +
+      '<span class="site-strip">' +
+      '  <input class="button strip-button strip-button-pin" type="button"' +
+      '   tabindex="-1" title="' + newTabString("pin") + '"/>' +
+      '  <input class="button strip-button strip-button-block" type="button"' +
+      '   tabindex="-1" title="' + newTabString("block") + '"/>' +
+      '</span>';
+
+    this._siteFragment = document.createDocumentFragment();
+    this._siteFragment.appendChild(site);
+  },
+
+  /**
+   * Draws the grid, creates all sites and puts them into their cells.
+   */
+  _draw: function Grid_draw() {
+    let cells = this.cells;
+
+    // Put sites into the cells.
+    let links = gLinks.getLinks();
+    let length = Math.min(links.length, cells.length);
+
+    for (let i = 0; i < length; i++) {
+      if (links[i])
+        this.createSite(links[i], cells[i]);
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/newTab.js
@@ -0,0 +1,47 @@
+/* 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";
+
+let Cu = Components.utils;
+let Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/PageThumbs.jsm");
+Cu.import("resource:///modules/NewTabUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect",
+  "resource://gre/modules/Geometry.jsm");
+
+let {
+  links: gLinks,
+  allPages: gAllPages,
+  pinnedLinks: gPinnedLinks,
+  blockedLinks: gBlockedLinks
+} = NewTabUtils;
+
+XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+  return Services.strings.
+    createBundle("chrome://browser/locale/newTab.properties");
+});
+
+function newTabString(name) gStringBundle.GetStringFromName('newtab.' + name);
+
+const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+const THUMB_WIDTH = 201;
+const THUMB_HEIGHT = 127;
+
+#include batch.js
+#include transformations.js
+#include page.js
+#include toolbar.js
+#include grid.js
+#include cells.js
+#include sites.js
+#include drag.js
+#include drop.js
+#include dropTargetShim.js
+#include dropPreview.js
+#include updater.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/newTab.xul
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?>
+
+<!DOCTYPE window [
+  <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
+  %newTabDTD;
+]>
+
+<xul:window xmlns="http://www.w3.org/1999/xhtml"
+            xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+            disablefastfind="true" title="&newtab.pageTitle;">
+  <xul:vbox id="scrollbox" flex="1" title=" ">
+    <body id="body">
+      <div id="toolbar">
+        <input class="button toolbar-button" id="toolbar-button-show"
+               type="button" title="&newtab.show;"/>
+        <input class="button toolbar-button" id="toolbar-button-hide"
+               type="button" title="&newtab.hide;"/>
+        <input class="button toolbar-button" id="toolbar-button-reset"
+               type="button" title="&newtab.reset;"/>
+      </div>
+
+      <ul id="grid">
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+      </ul>
+
+      <xul:script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/>
+      <xul:script type="text/javascript;version=1.8">
+        gPage.init("#toolbar", "#grid");
+      </xul:script>
+    </body>
+  </xul:vbox>
+</xul:window>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/page.js
@@ -0,0 +1,173 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton represents the whole 'New Tab Page' and takes care of
+ * initializing all its components.
+ */
+let gPage = {
+  /**
+   * Initializes the page.
+   * @param aToolbarSelector The query selector for the page toolbar.
+   * @param aGridSelector The query selector for the grid.
+   */
+  init: function Page_init(aToolbarSelector, aGridSelector) {
+    gToolbar.init(aToolbarSelector);
+    this._gridSelector = aGridSelector;
+
+    // Add ourselves to the list of pages to receive notifications.
+    gAllPages.register(this);
+
+    // Listen for 'unload' to unregister this page.
+    function unload() gAllPages.unregister(self);
+    addEventListener("unload", unload, false);
+
+    // Check if the new tab feature is enabled.
+    if (gAllPages.enabled)
+      this._init();
+    else
+      this._updateAttributes(false);
+  },
+
+  /**
+   * Listens for notifications specific to this page.
+   */
+  observe: function Page_observe() {
+    let enabled = gAllPages.enabled;
+    this._updateAttributes(enabled);
+
+    // Initialize the whole page if we haven't done that, yet.
+    if (enabled)
+      this._init();
+  },
+
+  /**
+   * Updates the whole page and the grid when the storage has changed.
+   */
+  update: function Page_update() {
+    this.updateModifiedFlag();
+    gGrid.refresh();
+  },
+
+  /**
+   * Checks if the page is modified and sets the CSS class accordingly
+   */
+  updateModifiedFlag: function Page_updateModifiedFlag() {
+    let node = document.getElementById("toolbar-button-reset");
+    let modified = this._isModified();
+
+    if (modified)
+      node.setAttribute("modified", "true");
+    else
+      node.removeAttribute("modified");
+
+    this._updateTabIndices(gAllPages.enabled, modified);
+  },
+
+  /**
+   * Internally initializes the page. This runs only when/if the feature
+   * is/gets enabled.
+   */
+  _init: function Page_init() {
+    if (this._initialized)
+      return;
+
+    this._initialized = true;
+
+    let self = this;
+
+    // Check if the grid is modified.
+    this.updateModifiedFlag();
+
+    // Initialize and render the grid.
+    gGrid.init(this._gridSelector);
+
+    // Initialize the drop target shim.
+    gDropTargetShim.init();
+
+    // Workaround to prevent a delay on MacOSX due to a slow drop animation.
+    let doc = document.documentElement;
+    doc.addEventListener("dragover", this.onDragOver, false);
+    doc.addEventListener("drop", this.onDrop, false);
+  },
+
+  /**
+   * Updates the 'page-disabled' attributes of the respective DOM nodes.
+   * @param aValue Whether to set or remove attributes.
+   */
+  _updateAttributes: function Page_updateAttributes(aValue) {
+    let nodes = document.querySelectorAll("#grid, #scrollbox, #toolbar, .toolbar-button");
+
+    // Set the nodes' states.
+    for (let i = 0; i < nodes.length; i++) {
+      let node = nodes[i];
+      if (aValue)
+        node.removeAttribute("page-disabled");
+      else
+        node.setAttribute("page-disabled", "true");
+    }
+
+    this._updateTabIndices(aValue, this._isModified());
+  },
+
+  /**
+   * Checks whether the page is modified.
+   * @return Whether the page is modified or not.
+   */
+  _isModified: function Page_isModified() {
+    // The page is considered modified only if sites have been removed.
+    return !gBlockedLinks.isEmpty();
+  },
+
+  /**
+   * Updates the tab indices of focusable elements.
+   * @param aEnabled Whether the page is currently enabled.
+   * @param aModified Whether the page is currently modified.
+   */
+  _updateTabIndices: function Page_updateTabIndices(aEnabled, aModified) {
+    function setFocusable(aNode, aFocusable) {
+      if (aFocusable)
+        aNode.removeAttribute("tabindex");
+      else
+        aNode.setAttribute("tabindex", "-1");
+    }
+
+    // Sites and the 'hide' button are always focusable when the grid is shown.
+    let nodes = document.querySelectorAll(".site, #toolbar-button-hide");
+    for (let i = 0; i < nodes.length; i++)
+      setFocusable(nodes[i], aEnabled);
+
+    // The 'show' button is focusable when the grid is hidden.
+    let btnShow = document.getElementById("toolbar-button-show");
+    setFocusable(btnShow, !aEnabled);
+
+    // The 'reset' button is focusable when the grid is shown and modified.
+    let btnReset = document.getElementById("toolbar-button-reset");
+    setFocusable(btnReset, aEnabled && aModified);
+  },
+
+  /**
+   * Handles the 'dragover' event. Workaround to prevent a delay on MacOSX
+   * due to a slow drop animation.
+   * @param aEvent The 'dragover' event.
+   */
+  onDragOver: function Page_onDragOver(aEvent) {
+    if (gDrag.isValid(aEvent))
+      aEvent.preventDefault();
+  },
+
+  /**
+   * Handles the 'drop' event. Workaround to prevent a delay on MacOSX due to
+   * a slow drop animation.
+   * @param aEvent The 'drop' event.
+   */
+  onDrop: function Page_onDrop(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      aEvent.stopPropagation();
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/sites.js
@@ -0,0 +1,207 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This class represents a site that is contained in a cell and can be pinned,
+ * moved around or deleted.
+ */
+function Site(aNode, aLink) {
+  this._node = aNode;
+  this._node._newtabSite = this;
+
+  this._link = aLink;
+
+  this._render();
+  this._addEventHandlers();
+}
+
+Site.prototype = {
+  /**
+   * The site's DOM node.
+   */
+  get node() this._node,
+
+  /**
+   * The site's link.
+   */
+  get link() this._link,
+
+  /**
+   * The url of the site's link.
+   */
+  get url() this.link.url,
+
+  /**
+   * The title of the site's link.
+   */
+  get title() this.link.title,
+
+  /**
+   * The site's parent cell.
+   */
+  get cell() {
+    let parentNode = this.node.parentNode;
+    return parentNode && parentNode._newtabCell;
+  },
+
+  /**
+   * Pins the site on its current or a given index.
+   * @param aIndex The pinned index (optional).
+   */
+  pin: function Site_pin(aIndex) {
+    if (typeof aIndex == "undefined")
+      aIndex = this.cell.index;
+
+    this._updateAttributes(true);
+    gPinnedLinks.pin(this._link, aIndex);
+  },
+
+  /**
+   * Unpins the site and calls the given callback when done.
+   * @param aCallback The callback to be called when finished.
+   */
+  unpin: function Site_unpin(aCallback) {
+    if (this.isPinned()) {
+      this._updateAttributes(false);
+      gPinnedLinks.unpin(this._link);
+      gUpdater.updateGrid(aCallback);
+    }
+  },
+
+  /**
+   * Checks whether this site is pinned.
+   * @return Whether this site is pinned.
+   */
+  isPinned: function Site_isPinned() {
+    return gPinnedLinks.isPinned(this._link);
+  },
+
+  /**
+   * Blocks the site (removes it from the grid) and calls the given callback
+   * when done.
+   * @param aCallback The callback to be called when finished.
+   */
+  block: function Site_block(aCallback) {
+    gBlockedLinks.block(this._link);
+    gUpdater.updateGrid(aCallback);
+    gPage.updateModifiedFlag();
+  },
+
+  /**
+   * Gets the DOM node specified by the given query selector.
+   * @param aSelector The query selector.
+   * @return The DOM node we found.
+   */
+  _querySelector: function Site_querySelector(aSelector) {
+    return this.node.querySelector(aSelector);
+  },
+
+  /**
+   * Updates attributes for all nodes which status depends on this site being
+   * pinned or unpinned.
+   * @param aPinned Whether this site is now pinned or unpinned.
+   */
+  _updateAttributes: function (aPinned) {
+    let buttonPin = this._querySelector(".strip-button-pin");
+
+    if (aPinned) {
+      this.node.setAttribute("pinned", true);
+      buttonPin.setAttribute("title", newTabString("unpin"));
+    } else {
+      this.node.removeAttribute("pinned");
+      buttonPin.setAttribute("title", newTabString("pin"));
+    }
+  },
+
+  /**
+   * Renders the site's data (fills the HTML fragment).
+   */
+  _render: function Site_render() {
+    this.node.setAttribute("href", this.url);
+    this._querySelector(".site-title").textContent = this.title || this.url;
+
+    if (this.isPinned())
+      this._updateAttributes(true);
+
+    this._renderThumbnail();
+  },
+
+  /**
+   * Renders the site's thumbnail.
+   */
+  _renderThumbnail: function Site_renderThumbnail() {
+    let img = this._querySelector(".site-img")
+    img.setAttribute("alt", this.title || this.url);
+    img.setAttribute("loading", "true");
+
+    // Wait until the image has loaded.
+    img.addEventListener("load", function onLoad() {
+      img.removeEventListener("load", onLoad, false);
+      img.removeAttribute("loading");
+    }, false);
+
+    // Set the thumbnail url.
+    img.setAttribute("src", PageThumbs.getThumbnailURL(this.url));
+  },
+
+  /**
+   * Adds event handlers for the site and its buttons.
+   */
+  _addEventHandlers: function Site_addEventHandlers() {
+    // Register drag-and-drop event handlers.
+    ["DragStart", /*"Drag",*/ "DragEnd"].forEach(function (aType) {
+      let method = "_on" + aType;
+      this[method] = this[method].bind(this);
+      this._node.addEventListener(aType.toLowerCase(), this[method], false);
+    }, this);
+
+    let self = this;
+
+    function pin(aEvent) {
+      if (aEvent)
+        aEvent.preventDefault();
+
+      if (self.isPinned())
+        self.unpin();
+      else
+        self.pin();
+    }
+
+    function block(aEvent) {
+      if (aEvent)
+        aEvent.preventDefault();
+
+      self.block();
+    }
+
+    this._querySelector(".strip-button-pin").addEventListener("click", pin, false);
+    this._querySelector(".strip-button-block").addEventListener("click", block, false);
+  },
+
+  /**
+   * Event handler for the 'dragstart' event.
+   * @param aEvent The drag event.
+   */
+  _onDragStart: function Site_onDragStart(aEvent) {
+    gDrag.start(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'drag' event.
+   * @param aEvent The drag event.
+  */
+  _onDrag: function Site_onDrag(aEvent) {
+    gDrag.drag(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'dragend' event.
+   * @param aEvent The drag event.
+   */
+  _onDragEnd: function Site_onDragEnd(aEvent) {
+    gDrag.end(this, aEvent);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/toolbar.js
@@ -0,0 +1,77 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton represents the page's toolbar that allows to enable/disable
+ * the 'New Tab Page' feature and to reset the whole page.
+ */
+let gToolbar = {
+  /**
+   * Initializes the toolbar.
+   * @param aSelector The query selector of the toolbar.
+   */
+  init: function Toolbar_init(aSelector) {
+    this._node = document.querySelector(aSelector);
+    let buttons = this._node.querySelectorAll("input");
+
+    // Listen for 'click' events on the toolbar buttons.
+    ["show", "hide", "reset"].forEach(function (aType, aIndex) {
+      let self = this;
+      let button = buttons[aIndex];
+      let handler = function () self[aType]();
+
+      button.addEventListener("click", handler, false);
+    }, this);
+  },
+
+  /**
+   * Enables the 'New Tab Page' feature.
+   */
+  show: function Toolbar_show() {
+    this._passButtonFocus("show", "hide");
+    gAllPages.enabled = true;
+  },
+
+  /**
+   * Disables the 'New Tab Page' feature.
+   */
+  hide: function Toolbar_hide() {
+    this._passButtonFocus("hide", "show");
+    gAllPages.enabled = false;
+  },
+
+  /**
+   * Resets the whole page and forces it to re-render its content.
+   * @param aCallback The callback to call when the page has been reset.
+   */
+  reset: function Toolbar_reset(aCallback) {
+    this._passButtonFocus("reset", "hide");
+    let node = gGrid.node;
+
+    // animate the page reset
+    gTransformation.fadeNodeOut(node, function () {
+      NewTabUtils.reset();
+
+      gLinks.populateCache(function () {
+        gAllPages.update();
+
+        // Without the setTimeout() we have a strange flicker.
+        setTimeout(function () gTransformation.fadeNodeIn(node, aCallback));
+      });
+    });
+  },
+
+  /**
+   * Passes the focus from the current button to the next.
+   * @param aCurrent The button that currently has focus.
+   * @param aNext The button that is focused next.
+   */
+  _passButtonFocus: function Toolbar_passButtonFocus(aCurrent, aNext) {
+    if (document.querySelector("#toolbar-button-" + aCurrent + ":-moz-focusring"))
+      document.getElementById("toolbar-button-" + aNext).focus();
+  }
+};
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/transformations.js
@@ -0,0 +1,226 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton allows to transform the grid by repositioning a site's node
+ * in the DOM and by showing or hiding the node. It additionally provides
+ * convenience methods to work with a site's DOM node.
+ */
+let gTransformation = {
+  /**
+   * Gets a DOM node's position.
+   * @param aNode The DOM node.
+   * @return A Rect instance with the position.
+   */
+  getNodePosition: function Transformation_getNodePosition(aNode) {
+    let {left, top, width, height} = aNode.getBoundingClientRect();
+    return new Rect(left + scrollX, top + scrollY, width, height);
+  },
+
+  /**
+   * Fades a given node from zero to full opacity.
+   * @param aNode The node to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  fadeNodeIn: function Transformation_fadeNodeIn(aNode, aCallback) {
+    this._setNodeOpacity(aNode, 1, function () {
+      // Clear the style property.
+      aNode.style.opacity = "";
+
+      if (aCallback)
+        aCallback();
+    });
+  },
+
+  /**
+   * Fades a given node from full to zero opacity.
+   * @param aNode The node to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  fadeNodeOut: function Transformation_fadeNodeOut(aNode, aCallback) {
+    this._setNodeOpacity(aNode, 0, aCallback);
+  },
+
+  /**
+   * Fades a given site from zero to full opacity.
+   * @param aSite The site to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  showSite: function Transformation_showSite(aSite, aCallback) {
+    this.fadeNodeIn(aSite.node, aCallback);
+  },
+
+  /**
+   * Fades a given site from full to zero opacity.
+   * @param aSite The site to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  hideSite: function Transformation_hideSite(aSite, aCallback) {
+    this.fadeNodeOut(aSite.node, aCallback);
+  },
+
+  /**
+   * Allows to set a site's position.
+   * @param aSite The site to re-position.
+   * @param aPosition The desired position for the given site.
+   */
+  setSitePosition: function Transformation_setSitePosition(aSite, aPosition) {
+    let style = aSite.node.style;
+    let {top, left} = aPosition;
+
+    style.top = top + "px";
+    style.left = left + "px";
+  },
+
+  /**
+   * Freezes a site in its current position by positioning it absolute.
+   * @param aSite The site to freeze.
+   */
+  freezeSitePosition: function Transformation_freezeSitePosition(aSite) {
+    aSite.node.setAttribute("frozen", "true");
+    this.setSitePosition(aSite, this.getNodePosition(aSite.node));
+  },
+
+  /**
+   * Unfreezes a site by removing its absolute positioning.
+   * @param aSite The site to unfreeze.
+   */
+  unfreezeSitePosition: function Transformation_unfreezeSitePosition(aSite) {
+    let style = aSite.node.style;
+    style.left = style.top = "";
+    aSite.node.removeAttribute("frozen");
+  },
+
+  /**
+   * Slides the given site to the target node's position.
+   * @param aSite The site to move.
+   * @param aTarget The slide target.
+   * @param aOptions Set of options (see below).
+   *        unfreeze - unfreeze the site after sliding
+   *        callback - the callback to call when finished
+   */
+  slideSiteTo: function Transformation_slideSiteTo(aSite, aTarget, aOptions) {
+    let currentPosition = this.getNodePosition(aSite.node);
+    let targetPosition = this.getNodePosition(aTarget.node)
+    let callback = aOptions && aOptions.callback;
+
+    let self = this;
+
+    function finish() {
+      if (aOptions && aOptions.unfreeze)
+        self.unfreezeSitePosition(aSite);
+
+      if (callback)
+        callback();
+    }
+
+    // Nothing to do here if the positions already match.
+    if (currentPosition.equals(targetPosition)) {
+      finish();
+    } else {
+      this.setSitePosition(aSite, targetPosition);
+      this._whenTransitionEnded(aSite.node, finish);
+    }
+  },
+
+  /**
+   * Rearranges a given array of sites and moves them to their new positions or
+   * fades in/out new/removed sites.
+   * @param aSites An array of sites to rearrange.
+   * @param aOptions Set of options (see below).
+   *        unfreeze - unfreeze the site after rearranging
+   *        callback - the callback to call when finished
+   */
+  rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
+    let batch;
+    let cells = gGrid.cells;
+    let callback = aOptions && aOptions.callback;
+    let unfreeze = aOptions && aOptions.unfreeze;
+
+    if (callback) {
+      batch = new Batch(callback);
+      callback = function () batch.pop();
+    }
+
+    aSites.forEach(function (aSite, aIndex) {
+      // Do not re-arrange empty cells or the dragged site.
+      if (!aSite || aSite == gDrag.draggedSite)
+        return;
+
+      if (batch)
+        batch.push();
+
+      if (!cells[aIndex])
+        // The site disappeared from the grid, hide it.
+        this.hideSite(aSite, callback);
+      else if (this._getNodeOpacity(aSite.node) != 1)
+        // The site disappeared before but is now back, show it.
+        this.showSite(aSite, callback);
+      else
+        // The site's position has changed, move it around.
+        this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: callback});
+    }, this);
+
+    if (batch)
+      batch.close();
+  },
+
+  /**
+   * Listens for the 'transitionend' event on a given node and calls the given
+   * callback.
+   * @param aNode The node that is transitioned.
+   * @param aCallback The callback to call when finished.
+   */
+  _whenTransitionEnded:
+    function Transformation_whenTransitionEnded(aNode, aCallback) {
+
+    aNode.addEventListener("transitionend", function onEnd() {
+      aNode.removeEventListener("transitionend", onEnd, false);
+      aCallback();
+    }, false);
+  },
+
+  /**
+   * Gets a given node's opacity value.
+   * @param aNode The node to get the opacity value from.
+   * @return The node's opacity value.
+   */
+  _getNodeOpacity: function Transformation_getNodeOpacity(aNode) {
+    let cstyle = window.getComputedStyle(aNode, null);
+    return cstyle.getPropertyValue("opacity");
+  },
+
+  /**
+   * Sets a given node's opacity.
+   * @param aNode The node to set the opacity value for.
+   * @param aOpacity The opacity value to set.
+   * @param aCallback The callback to call when finished.
+   */
+  _setNodeOpacity:
+    function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) {
+
+    if (this._getNodeOpacity(aNode) == aOpacity) {
+      if (aCallback)
+        aCallback();
+    } else {
+      if (aCallback)
+        this._whenTransitionEnded(aNode, aCallback);
+
+      aNode.style.opacity = aOpacity;
+    }
+  },
+
+  /**
+   * Moves a site to the cell with the given index.
+   * @param aSite The site to move.
+   * @param aIndex The target cell's index.
+   * @param aOptions Options that are directly passed to slideSiteTo().
+   */
+  _moveSite: function Transformation_moveSite(aSite, aIndex, aOptions) {
+    this.freezeSitePosition(aSite);
+    this.slideSiteTo(aSite, gGrid.cells[aIndex], aOptions);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/updater.js
@@ -0,0 +1,182 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * This singleton provides functionality to update the current grid to a new
+ * set of pinned and blocked sites. It adds, moves and removes sites.
+ */
+let gUpdater = {
+  /**
+   * Updates the current grid according to its pinned and blocked sites.
+   * This removes old, moves existing and creates new sites to fill gaps.
+   * @param aCallback The callback to call when finished.
+   */
+  updateGrid: function Updater_updateGrid(aCallback) {
+    let links = gLinks.getLinks().slice(0, gGrid.cells.length);
+
+    // Find all sites that remain in the grid.
+    let sites = this._findRemainingSites(links);
+
+    let self = this;
+
+    // Remove sites that are no longer in the grid.
+    this._removeLegacySites(sites, function () {
+      // Freeze all site positions so that we can move their DOM nodes around
+      // without any visual impact.
+      self._freezeSitePositions(sites);
+
+      // Move the sites' DOM nodes to their new position in the DOM. This will
+      // have no visual effect as all the sites have been frozen and will
+      // remain in their current position.
+      self._moveSiteNodes(sites);
+
+      // Now it's time to animate the sites actually moving to their new
+      // positions.
+      self._rearrangeSites(sites, function () {
+        // Try to fill empty cells and finish.
+        self._fillEmptyCells(links, aCallback);
+
+        // Update other pages that might be open to keep them synced.
+        gAllPages.update(gPage);
+      });
+    });
+  },
+
+  /**
+   * Takes an array of links and tries to correlate them to sites contained in
+   * the current grid. If no corresponding site can be found (i.e. the link is
+   * new and a site will be created) then just set it to null.
+   * @param aLinks The array of links to find sites for.
+   * @return Array of sites mapped to the given links (can contain null values).
+   */
+  _findRemainingSites: function Updater_findRemainingSites(aLinks) {
+    let map = {};
+
+    // Create a map to easily retrieve the site for a given URL.
+    gGrid.sites.forEach(function (aSite) {
+      if (aSite)
+        map[aSite.url] = aSite;
+    });
+
+    // Map each link to its corresponding site, if any.
+    return aLinks.map(function (aLink) {
+      return aLink && (aLink.url in map) && map[aLink.url];
+    });
+  },
+
+  /**
+   * Freezes the given sites' positions.
+   * @param aSites The array of sites to freeze.
+   */
+  _freezeSitePositions: function Updater_freezeSitePositions(aSites) {
+    aSites.forEach(function (aSite) {
+      if (aSite)
+        gTransformation.freezeSitePosition(aSite);
+    });
+  },
+
+  /**
+   * Moves the given sites' DOM nodes to their new positions.
+   * @param aSites The array of sites to move.
+   */
+  _moveSiteNodes: function Updater_moveSiteNodes(aSites) {
+    let cells = gGrid.cells;
+
+    // Truncate the given array of sites to not have more sites than cells.
+    // This can happen when the user drags a bookmark (or any other new kind
+    // of link) onto the grid.
+    let sites = aSites.slice(0, cells.length);
+
+    sites.forEach(function (aSite, aIndex) {
+      let cell = cells[aIndex];
+      let cellSite = cell.site;
+
+      // The site's position didn't change.
+      if (!aSite || cellSite != aSite) {
+        let cellNode = cell.node;
+
+        // Empty the cell if necessary.
+        if (cellSite)
+          cellNode.removeChild(cellSite.node);
+
+        // Put the new site in place, if any.
+        if (aSite)
+          cellNode.appendChild(aSite.node);
+      }
+    }, this);
+  },
+
+  /**
+   * Rearranges the given sites and slides them to their new positions.
+   * @param aSites The array of sites to re-arrange.
+   * @param aCallback The callback to call when finished.
+   */
+  _rearrangeSites: function Updater_rearrangeSites(aSites, aCallback) {
+    let options = {callback: aCallback, unfreeze: true};
+    gTransformation.rearrangeSites(aSites, options);
+  },
+
+  /**
+   * Removes all sites from the grid that are not in the given links array or
+   * exceed the grid.
+   * @param aSites The array of sites remaining in the grid.
+   * @param aCallback The callback to call when finished.
+   */
+  _removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
+    let batch = new Batch(aCallback);
+
+    // Delete sites that were removed from the grid.
+    gGrid.sites.forEach(function (aSite) {
+      // The site must be valid and not in the current grid.
+      if (!aSite || aSites.indexOf(aSite) != -1)
+        return;
+
+      batch.push();
+
+      // Fade out the to-be-removed site.
+      gTransformation.hideSite(aSite, function () {
+        let node = aSite.node;
+
+        // Remove the site from the DOM.
+        node.parentNode.removeChild(node);
+        batch.pop();
+      });
+    });
+
+    batch.close();
+  },
+
+  /**
+   * Tries to fill empty cells with new links if available.
+   * @param aLinks The array of links.
+   * @param aCallback The callback to call when finished.
+   */
+  _fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
+    let {cells, sites} = gGrid;
+    let batch = new Batch(aCallback);
+
+    // Find empty cells and fill them.
+    sites.forEach(function (aSite, aIndex) {
+      if (aSite || !aLinks[aIndex])
+        return;
+
+      batch.push();
+
+      // Create the new site and fade it in.
+      let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
+
+      // Set the site's initial opacity to zero.
+      site.node.style.opacity = 0;
+
+      // Without the setTimeout() the node would just appear instead of fade in.
+      setTimeout(function () {
+        gTransformation.showSite(site, function () batch.pop());
+      }, 0);
+    });
+
+    batch.close();
+  }
+};