browser/components/customizableui/src/DragPositionManager.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 Components.utils.import("resource:///modules/CustomizableUI.jsm");
michael@0 8
michael@0 9 let gManagers = new WeakMap();
michael@0 10
michael@0 11 const kPaletteId = "customization-palette";
michael@0 12 const kPlaceholderClass = "panel-customization-placeholder";
michael@0 13
michael@0 14 this.EXPORTED_SYMBOLS = ["DragPositionManager"];
michael@0 15
michael@0 16 function AreaPositionManager(aContainer) {
michael@0 17 // Caching the direction and bounds of the container for quick access later:
michael@0 18 let window = aContainer.ownerDocument.defaultView;
michael@0 19 this._dir = window.getComputedStyle(aContainer).direction;
michael@0 20 let containerRect = aContainer.getBoundingClientRect();
michael@0 21 this._containerInfo = {
michael@0 22 left: containerRect.left,
michael@0 23 right: containerRect.right,
michael@0 24 top: containerRect.top,
michael@0 25 width: containerRect.width
michael@0 26 };
michael@0 27 this._inPanel = aContainer.id == CustomizableUI.AREA_PANEL;
michael@0 28 this._horizontalDistance = null;
michael@0 29 this.update(aContainer);
michael@0 30 }
michael@0 31
michael@0 32 AreaPositionManager.prototype = {
michael@0 33 _nodePositionStore: null,
michael@0 34 _wideCache: null,
michael@0 35
michael@0 36 update: function(aContainer) {
michael@0 37 let window = aContainer.ownerDocument.defaultView;
michael@0 38 this._nodePositionStore = new WeakMap();
michael@0 39 this._wideCache = new Set();
michael@0 40 let last = null;
michael@0 41 let singleItemHeight;
michael@0 42 for (let child of aContainer.children) {
michael@0 43 if (child.hidden) {
michael@0 44 continue;
michael@0 45 }
michael@0 46 let isNodeWide = this._checkIfWide(child);
michael@0 47 if (isNodeWide) {
michael@0 48 this._wideCache.add(child.id);
michael@0 49 }
michael@0 50 let coordinates = this._lazyStoreGet(child);
michael@0 51 // We keep a baseline horizontal distance between non-wide nodes around
michael@0 52 // for use when we can't compare with previous/next nodes
michael@0 53 if (!this._horizontalDistance && last && !isNodeWide) {
michael@0 54 this._horizontalDistance = coordinates.left - last.left;
michael@0 55 }
michael@0 56 // We also keep the basic height of non-wide items for use below:
michael@0 57 if (!isNodeWide && !singleItemHeight) {
michael@0 58 singleItemHeight = coordinates.height;
michael@0 59 }
michael@0 60 last = !isNodeWide ? coordinates : null;
michael@0 61 }
michael@0 62 if (this._inPanel) {
michael@0 63 this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT;
michael@0 64 } else {
michael@0 65 this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
michael@0 66 }
michael@0 67 },
michael@0 68
michael@0 69 /**
michael@0 70 * Find the closest node in the container given the coordinates.
michael@0 71 * "Closest" is defined in a somewhat strange manner: we prefer nodes
michael@0 72 * which are in the same row over nodes that are in a different row.
michael@0 73 * In order to implement this, we use a weighted cartesian distance
michael@0 74 * where dy is more heavily weighted by a factor corresponding to the
michael@0 75 * ratio between the container's width and the height of its elements.
michael@0 76 */
michael@0 77 find: function(aContainer, aX, aY, aDraggedItemId) {
michael@0 78 let closest = null;
michael@0 79 let minCartesian = Number.MAX_VALUE;
michael@0 80 let containerX = this._containerInfo.left;
michael@0 81 let containerY = this._containerInfo.top;
michael@0 82 for (let node of aContainer.children) {
michael@0 83 let coordinates = this._lazyStoreGet(node);
michael@0 84 let offsetX = coordinates.x - containerX;
michael@0 85 let offsetY = coordinates.y - containerY;
michael@0 86 let hDiff = offsetX - aX;
michael@0 87 let vDiff = offsetY - aY;
michael@0 88 // For wide widgets, we're always going to be further from the center
michael@0 89 // horizontally. Compensate:
michael@0 90 if (this.isWide(node)) {
michael@0 91 hDiff /= CustomizableUI.PANEL_COLUMN_COUNT;
michael@0 92 }
michael@0 93 // Then compensate for the height/width ratio so that we prefer items
michael@0 94 // which are in the same row:
michael@0 95 hDiff /= this._heightToWidthFactor;
michael@0 96
michael@0 97 let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
michael@0 98 if (cartesianDiff < minCartesian) {
michael@0 99 minCartesian = cartesianDiff;
michael@0 100 closest = node;
michael@0 101 }
michael@0 102 }
michael@0 103
michael@0 104 // Now correct this node based on what we're dragging
michael@0 105 if (closest) {
michael@0 106 let doc = aContainer.ownerDocument;
michael@0 107 let draggedItem = doc.getElementById(aDraggedItemId);
michael@0 108 // If dragging a wide item, always pick the first item in a row:
michael@0 109 if (this._inPanel && draggedItem &&
michael@0 110 draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
michael@0 111 return this._firstInRow(closest);
michael@0 112 }
michael@0 113 let targetBounds = this._lazyStoreGet(closest);
michael@0 114 let farSide = this._dir == "ltr" ? "right" : "left";
michael@0 115 let outsideX = targetBounds[farSide];
michael@0 116 // Check if we're closer to the next target than to this one:
michael@0 117 // Only move if we're not targeting a node in a different row:
michael@0 118 if (aY > targetBounds.top && aY < targetBounds.bottom) {
michael@0 119 if ((this._dir == "ltr" && aX > outsideX) ||
michael@0 120 (this._dir == "rtl" && aX < outsideX)) {
michael@0 121 return closest.nextSibling || aContainer;
michael@0 122 }
michael@0 123 }
michael@0 124 }
michael@0 125 return closest;
michael@0 126 },
michael@0 127
michael@0 128 /**
michael@0 129 * "Insert" a "placeholder" by shifting the subsequent children out of the
michael@0 130 * way. We go through all the children, and shift them based on the position
michael@0 131 * they would have if we had inserted something before aBefore. We use CSS
michael@0 132 * transforms for this, which are CSS transitioned.
michael@0 133 */
michael@0 134 insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) {
michael@0 135 let isShifted = false;
michael@0 136 let shiftDown = aWide;
michael@0 137 for (let child of aContainer.children) {
michael@0 138 // Don't need to shift hidden nodes:
michael@0 139 if (child.getAttribute("hidden") == "true") {
michael@0 140 continue;
michael@0 141 }
michael@0 142 // If this is the node before which we're inserting, start shifting
michael@0 143 // everything that comes after. One exception is inserting at the end
michael@0 144 // of the menupanel, in which case we do not shift the placeholders:
michael@0 145 if (child == aBefore && !child.classList.contains(kPlaceholderClass)) {
michael@0 146 isShifted = true;
michael@0 147 // If the node before which we're inserting is wide, we should
michael@0 148 // shift everything one row down:
michael@0 149 if (!shiftDown && this.isWide(child)) {
michael@0 150 shiftDown = true;
michael@0 151 }
michael@0 152 }
michael@0 153 // If we're moving items before a wide node that were already there,
michael@0 154 // it's possible it's not necessary to shift nodes
michael@0 155 // including & after the wide node.
michael@0 156 if (this.__undoShift) {
michael@0 157 isShifted = false;
michael@0 158 }
michael@0 159 if (isShifted) {
michael@0 160 // Conversely, if we're adding something before a wide node, for
michael@0 161 // simplicity's sake we move everything including the wide node down:
michael@0 162 if (this.__moveDown) {
michael@0 163 shiftDown = true;
michael@0 164 }
michael@0 165 if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
michael@0 166 child.setAttribute("notransition", "true");
michael@0 167 }
michael@0 168 // Determine the CSS transform based on the next node:
michael@0 169 child.style.transform = this._getNextPos(child, shiftDown, aSize);
michael@0 170 } else {
michael@0 171 // If we're not shifting this node, reset the transform
michael@0 172 child.style.transform = "";
michael@0 173 }
michael@0 174 }
michael@0 175 if (aContainer.lastChild && aIsFromThisArea &&
michael@0 176 !this._lastPlaceholderInsertion) {
michael@0 177 // Flush layout:
michael@0 178 aContainer.lastChild.getBoundingClientRect();
michael@0 179 // then remove all the [notransition]
michael@0 180 for (let child of aContainer.children) {
michael@0 181 child.removeAttribute("notransition");
michael@0 182 }
michael@0 183 }
michael@0 184 delete this.__moveDown;
michael@0 185 delete this.__undoShift;
michael@0 186 this._lastPlaceholderInsertion = aBefore;
michael@0 187 },
michael@0 188
michael@0 189 isWide: function(aNode) {
michael@0 190 return this._wideCache.has(aNode.id);
michael@0 191 },
michael@0 192
michael@0 193 _checkIfWide: function(aNode) {
michael@0 194 return this._inPanel && aNode && aNode.firstChild &&
michael@0 195 aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
michael@0 196 },
michael@0 197
michael@0 198 /**
michael@0 199 * Reset all the transforms in this container, optionally without
michael@0 200 * transitioning them.
michael@0 201 * @param aContainer the container in which to reset transforms
michael@0 202 * @param aNoTransition if truthy, adds a notransition attribute to the node
michael@0 203 * while resetting the transform.
michael@0 204 */
michael@0 205 clearPlaceholders: function(aContainer, aNoTransition) {
michael@0 206 for (let child of aContainer.children) {
michael@0 207 if (aNoTransition) {
michael@0 208 child.setAttribute("notransition", true);
michael@0 209 }
michael@0 210 child.style.transform = "";
michael@0 211 if (aNoTransition) {
michael@0 212 // Need to force a reflow otherwise this won't work.
michael@0 213 child.getBoundingClientRect();
michael@0 214 child.removeAttribute("notransition");
michael@0 215 }
michael@0 216 }
michael@0 217 // We snapped back, so we can assume there's no more
michael@0 218 // "last" placeholder insertion point to keep track of.
michael@0 219 if (aNoTransition) {
michael@0 220 this._lastPlaceholderInsertion = null;
michael@0 221 }
michael@0 222 },
michael@0 223
michael@0 224 _getNextPos: function(aNode, aShiftDown, aSize) {
michael@0 225 // Shifting down is easy:
michael@0 226 if (this._inPanel && aShiftDown) {
michael@0 227 return "translate(0, " + aSize.height + "px)";
michael@0 228 }
michael@0 229 return this._diffWithNext(aNode, aSize);
michael@0 230 },
michael@0 231
michael@0 232 _diffWithNext: function(aNode, aSize) {
michael@0 233 let xDiff;
michael@0 234 let yDiff = null;
michael@0 235 let nodeBounds = this._lazyStoreGet(aNode);
michael@0 236 let side = this._dir == "ltr" ? "left" : "right";
michael@0 237 let next = this._getVisibleSiblingForDirection(aNode, "next");
michael@0 238 // First we determine the transform along the x axis.
michael@0 239 // Usually, there will be a next node to base this on:
michael@0 240 if (next) {
michael@0 241 let otherBounds = this._lazyStoreGet(next);
michael@0 242 xDiff = otherBounds[side] - nodeBounds[side];
michael@0 243 // If the next node is a wide item in the panel, check if we could maybe
michael@0 244 // just move further out in the same row, without snapping to the next
michael@0 245 // one. This happens, for example, if moving an item that's before a wide
michael@0 246 // node within its own row of items. There will be space to drop this
michael@0 247 // item within the row, and the rest of the items do not need to shift.
michael@0 248 if (this.isWide(next)) {
michael@0 249 let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds,
michael@0 250 this._firstInRow(aNode));
michael@0 251 // If this has the same sign as our original shift, we're still
michael@0 252 // snapping to the start of the row. In this case, we should move
michael@0 253 // everything after us a row down, so as not to display two nodes on
michael@0 254 // top of each other:
michael@0 255 // (we would be able to get away with checking for equality instead of
michael@0 256 // equal signs here, but one of these is based on the x coordinate of
michael@0 257 // the first item in row N and one on that for row N - 1, so this is
michael@0 258 // safer, as their margins might differ)
michael@0 259 if ((otherXDiff < 0) == (xDiff < 0)) {
michael@0 260 this.__moveDown = true;
michael@0 261 } else {
michael@0 262 // Otherwise, we succeeded and can move further out. This also means
michael@0 263 // we can stop shifting the rest of the content:
michael@0 264 xDiff = otherXDiff;
michael@0 265 this.__undoShift = true;
michael@0 266 }
michael@0 267 } else {
michael@0 268 // We set this explicitly because otherwise some strange difference
michael@0 269 // between the height and the actual difference between line creeps in
michael@0 270 // and messes with alignments
michael@0 271 yDiff = otherBounds.top - nodeBounds.top;
michael@0 272 }
michael@0 273 } else {
michael@0 274 // We don't have a sibling whose position we can use. First, let's see
michael@0 275 // if we're also the first item (which complicates things):
michael@0 276 let firstNode = this._firstInRow(aNode);
michael@0 277 if (aNode == firstNode) {
michael@0 278 // Maybe we stored the horizontal distance between non-wide nodes,
michael@0 279 // if not, we'll use the width of the incoming node as a proxy:
michael@0 280 xDiff = this._horizontalDistance || aSize.width;
michael@0 281 } else {
michael@0 282 // If not, we should be able to get the distance to the previous node
michael@0 283 // and use the inverse, unless there's no room for another node (ie we
michael@0 284 // are the last node and there's no room for another one)
michael@0 285 xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
michael@0 286 }
michael@0 287 }
michael@0 288
michael@0 289 // If we've not determined the vertical difference yet, check it here
michael@0 290 if (yDiff === null) {
michael@0 291 // If the next node is behind rather than in front, we must have moved
michael@0 292 // vertically:
michael@0 293 if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) {
michael@0 294 yDiff = aSize.height;
michael@0 295 } else {
michael@0 296 // Otherwise, we haven't
michael@0 297 yDiff = 0;
michael@0 298 }
michael@0 299 }
michael@0 300 return "translate(" + xDiff + "px, " + yDiff + "px)";
michael@0 301 },
michael@0 302
michael@0 303 /**
michael@0 304 * Helper function to find the transform a node if there isn't a next node
michael@0 305 * to base that on.
michael@0 306 * @param aNode the node to transform
michael@0 307 * @param aNodeBounds the bounding rect info of this node
michael@0 308 * @param aFirstNodeInRow the first node in aNode's row
michael@0 309 */
michael@0 310 _moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) {
michael@0 311 let next = this._getVisibleSiblingForDirection(aNode, "previous");
michael@0 312 let otherBounds = this._lazyStoreGet(next);
michael@0 313 let side = this._dir == "ltr" ? "left" : "right";
michael@0 314 let xDiff = aNodeBounds[side] - otherBounds[side];
michael@0 315 // If, however, this means we move outside the container's box
michael@0 316 // (i.e. the row in which this item is placed is full)
michael@0 317 // we should move it to align with the first item in the next row instead
michael@0 318 let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"];
michael@0 319 if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) ||
michael@0 320 (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) {
michael@0 321 xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
michael@0 322 }
michael@0 323 return xDiff;
michael@0 324 },
michael@0 325
michael@0 326 /**
michael@0 327 * Get position details from our cache. If the node is not yet cached, get its position
michael@0 328 * information and cache it now.
michael@0 329 * @param aNode the node whose position info we want
michael@0 330 * @return the position info
michael@0 331 */
michael@0 332 _lazyStoreGet: function(aNode) {
michael@0 333 let rect = this._nodePositionStore.get(aNode);
michael@0 334 if (!rect) {
michael@0 335 // getBoundingClientRect() returns a DOMRect that is live, meaning that
michael@0 336 // as the element moves around, the rects values change. We don't want
michael@0 337 // that - we want a snapshot of what the rect values are right at this
michael@0 338 // moment, and nothing else. So we have to clone the values.
michael@0 339 let clientRect = aNode.getBoundingClientRect();
michael@0 340 rect = {
michael@0 341 left: clientRect.left,
michael@0 342 right: clientRect.right,
michael@0 343 width: clientRect.width,
michael@0 344 height: clientRect.height,
michael@0 345 top: clientRect.top,
michael@0 346 bottom: clientRect.bottom,
michael@0 347 };
michael@0 348 rect.x = rect.left + rect.width / 2;
michael@0 349 rect.y = rect.top + rect.height / 2;
michael@0 350 Object.freeze(rect);
michael@0 351 this._nodePositionStore.set(aNode, rect);
michael@0 352 }
michael@0 353 return rect;
michael@0 354 },
michael@0 355
michael@0 356 _firstInRow: function(aNode) {
michael@0 357 // XXXmconley: I'm not entirely sure why we need to take the floor of these
michael@0 358 // values - it looks like, periodically, we're getting fractional pixels back
michael@0 359 //from lazyStoreGet. I've filed bug 994247 to investigate.
michael@0 360 let bound = Math.floor(this._lazyStoreGet(aNode).top);
michael@0 361 let rv = aNode;
michael@0 362 let prev;
michael@0 363 while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
michael@0 364 if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
michael@0 365 return rv;
michael@0 366 }
michael@0 367 rv = prev;
michael@0 368 }
michael@0 369 return rv;
michael@0 370 },
michael@0 371
michael@0 372 _getVisibleSiblingForDirection: function(aNode, aDirection) {
michael@0 373 let rv = aNode;
michael@0 374 do {
michael@0 375 rv = rv[aDirection + "Sibling"];
michael@0 376 } while (rv && rv.getAttribute("hidden") == "true")
michael@0 377 return rv;
michael@0 378 }
michael@0 379 }
michael@0 380
michael@0 381 let DragPositionManager = {
michael@0 382 start: function(aWindow) {
michael@0 383 let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar");
michael@0 384 areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow));
michael@0 385 areas.push(aWindow.document.getElementById(kPaletteId));
michael@0 386 for (let areaNode of areas) {
michael@0 387 let positionManager = gManagers.get(areaNode);
michael@0 388 if (positionManager) {
michael@0 389 positionManager.update(areaNode);
michael@0 390 } else {
michael@0 391 gManagers.set(areaNode, new AreaPositionManager(areaNode));
michael@0 392 }
michael@0 393 }
michael@0 394 },
michael@0 395
michael@0 396 add: function(aWindow, aArea, aContainer) {
michael@0 397 if (CustomizableUI.getAreaType(aArea) != "toolbar") {
michael@0 398 return;
michael@0 399 }
michael@0 400
michael@0 401 gManagers.set(aContainer, new AreaPositionManager(aContainer));
michael@0 402 },
michael@0 403
michael@0 404 remove: function(aWindow, aArea, aContainer) {
michael@0 405 if (CustomizableUI.getAreaType(aArea) != "toolbar") {
michael@0 406 return;
michael@0 407 }
michael@0 408
michael@0 409 gManagers.delete(aContainer);
michael@0 410 },
michael@0 411
michael@0 412 stop: function() {
michael@0 413 gManagers.clear();
michael@0 414 },
michael@0 415
michael@0 416 getManagerForArea: function(aArea) {
michael@0 417 return gManagers.get(aArea);
michael@0 418 }
michael@0 419 };
michael@0 420
michael@0 421 Object.freeze(DragPositionManager);
michael@0 422

mercurial