Bug 455553 - Part 1 - XUL/HTML Page and Scripts; r=blair,dietrich

This commit is contained in:
Tim Taubert 2012-01-25 23:40:18 +01:00
parent 642529ebd6
commit ff67d1da75
14 changed files with 1986 additions and 0 deletions

View File

@ -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;
}
}
};

View File

@ -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);
}
}
};

View File

@ -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);
}
};

View File

@ -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});
}
};

View File

@ -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;
}
};

View File

@ -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);
}
};

View File

@ -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]);
}
}
};

View File

@ -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

View File

@ -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>

View File

@ -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();
}
}
};

View File

@ -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);
}
};

View File

@ -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();
}
};

View File

@ -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);
}
};

View File

@ -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();
}
};