1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/customizableui/src/DragPositionManager.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,422 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +Components.utils.import("resource:///modules/CustomizableUI.jsm"); 1.11 + 1.12 +let gManagers = new WeakMap(); 1.13 + 1.14 +const kPaletteId = "customization-palette"; 1.15 +const kPlaceholderClass = "panel-customization-placeholder"; 1.16 + 1.17 +this.EXPORTED_SYMBOLS = ["DragPositionManager"]; 1.18 + 1.19 +function AreaPositionManager(aContainer) { 1.20 + // Caching the direction and bounds of the container for quick access later: 1.21 + let window = aContainer.ownerDocument.defaultView; 1.22 + this._dir = window.getComputedStyle(aContainer).direction; 1.23 + let containerRect = aContainer.getBoundingClientRect(); 1.24 + this._containerInfo = { 1.25 + left: containerRect.left, 1.26 + right: containerRect.right, 1.27 + top: containerRect.top, 1.28 + width: containerRect.width 1.29 + }; 1.30 + this._inPanel = aContainer.id == CustomizableUI.AREA_PANEL; 1.31 + this._horizontalDistance = null; 1.32 + this.update(aContainer); 1.33 +} 1.34 + 1.35 +AreaPositionManager.prototype = { 1.36 + _nodePositionStore: null, 1.37 + _wideCache: null, 1.38 + 1.39 + update: function(aContainer) { 1.40 + let window = aContainer.ownerDocument.defaultView; 1.41 + this._nodePositionStore = new WeakMap(); 1.42 + this._wideCache = new Set(); 1.43 + let last = null; 1.44 + let singleItemHeight; 1.45 + for (let child of aContainer.children) { 1.46 + if (child.hidden) { 1.47 + continue; 1.48 + } 1.49 + let isNodeWide = this._checkIfWide(child); 1.50 + if (isNodeWide) { 1.51 + this._wideCache.add(child.id); 1.52 + } 1.53 + let coordinates = this._lazyStoreGet(child); 1.54 + // We keep a baseline horizontal distance between non-wide nodes around 1.55 + // for use when we can't compare with previous/next nodes 1.56 + if (!this._horizontalDistance && last && !isNodeWide) { 1.57 + this._horizontalDistance = coordinates.left - last.left; 1.58 + } 1.59 + // We also keep the basic height of non-wide items for use below: 1.60 + if (!isNodeWide && !singleItemHeight) { 1.61 + singleItemHeight = coordinates.height; 1.62 + } 1.63 + last = !isNodeWide ? coordinates : null; 1.64 + } 1.65 + if (this._inPanel) { 1.66 + this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT; 1.67 + } else { 1.68 + this._heightToWidthFactor = this._containerInfo.width / singleItemHeight; 1.69 + } 1.70 + }, 1.71 + 1.72 + /** 1.73 + * Find the closest node in the container given the coordinates. 1.74 + * "Closest" is defined in a somewhat strange manner: we prefer nodes 1.75 + * which are in the same row over nodes that are in a different row. 1.76 + * In order to implement this, we use a weighted cartesian distance 1.77 + * where dy is more heavily weighted by a factor corresponding to the 1.78 + * ratio between the container's width and the height of its elements. 1.79 + */ 1.80 + find: function(aContainer, aX, aY, aDraggedItemId) { 1.81 + let closest = null; 1.82 + let minCartesian = Number.MAX_VALUE; 1.83 + let containerX = this._containerInfo.left; 1.84 + let containerY = this._containerInfo.top; 1.85 + for (let node of aContainer.children) { 1.86 + let coordinates = this._lazyStoreGet(node); 1.87 + let offsetX = coordinates.x - containerX; 1.88 + let offsetY = coordinates.y - containerY; 1.89 + let hDiff = offsetX - aX; 1.90 + let vDiff = offsetY - aY; 1.91 + // For wide widgets, we're always going to be further from the center 1.92 + // horizontally. Compensate: 1.93 + if (this.isWide(node)) { 1.94 + hDiff /= CustomizableUI.PANEL_COLUMN_COUNT; 1.95 + } 1.96 + // Then compensate for the height/width ratio so that we prefer items 1.97 + // which are in the same row: 1.98 + hDiff /= this._heightToWidthFactor; 1.99 + 1.100 + let cartesianDiff = hDiff * hDiff + vDiff * vDiff; 1.101 + if (cartesianDiff < minCartesian) { 1.102 + minCartesian = cartesianDiff; 1.103 + closest = node; 1.104 + } 1.105 + } 1.106 + 1.107 + // Now correct this node based on what we're dragging 1.108 + if (closest) { 1.109 + let doc = aContainer.ownerDocument; 1.110 + let draggedItem = doc.getElementById(aDraggedItemId); 1.111 + // If dragging a wide item, always pick the first item in a row: 1.112 + if (this._inPanel && draggedItem && 1.113 + draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { 1.114 + return this._firstInRow(closest); 1.115 + } 1.116 + let targetBounds = this._lazyStoreGet(closest); 1.117 + let farSide = this._dir == "ltr" ? "right" : "left"; 1.118 + let outsideX = targetBounds[farSide]; 1.119 + // Check if we're closer to the next target than to this one: 1.120 + // Only move if we're not targeting a node in a different row: 1.121 + if (aY > targetBounds.top && aY < targetBounds.bottom) { 1.122 + if ((this._dir == "ltr" && aX > outsideX) || 1.123 + (this._dir == "rtl" && aX < outsideX)) { 1.124 + return closest.nextSibling || aContainer; 1.125 + } 1.126 + } 1.127 + } 1.128 + return closest; 1.129 + }, 1.130 + 1.131 + /** 1.132 + * "Insert" a "placeholder" by shifting the subsequent children out of the 1.133 + * way. We go through all the children, and shift them based on the position 1.134 + * they would have if we had inserted something before aBefore. We use CSS 1.135 + * transforms for this, which are CSS transitioned. 1.136 + */ 1.137 + insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) { 1.138 + let isShifted = false; 1.139 + let shiftDown = aWide; 1.140 + for (let child of aContainer.children) { 1.141 + // Don't need to shift hidden nodes: 1.142 + if (child.getAttribute("hidden") == "true") { 1.143 + continue; 1.144 + } 1.145 + // If this is the node before which we're inserting, start shifting 1.146 + // everything that comes after. One exception is inserting at the end 1.147 + // of the menupanel, in which case we do not shift the placeholders: 1.148 + if (child == aBefore && !child.classList.contains(kPlaceholderClass)) { 1.149 + isShifted = true; 1.150 + // If the node before which we're inserting is wide, we should 1.151 + // shift everything one row down: 1.152 + if (!shiftDown && this.isWide(child)) { 1.153 + shiftDown = true; 1.154 + } 1.155 + } 1.156 + // If we're moving items before a wide node that were already there, 1.157 + // it's possible it's not necessary to shift nodes 1.158 + // including & after the wide node. 1.159 + if (this.__undoShift) { 1.160 + isShifted = false; 1.161 + } 1.162 + if (isShifted) { 1.163 + // Conversely, if we're adding something before a wide node, for 1.164 + // simplicity's sake we move everything including the wide node down: 1.165 + if (this.__moveDown) { 1.166 + shiftDown = true; 1.167 + } 1.168 + if (aIsFromThisArea && !this._lastPlaceholderInsertion) { 1.169 + child.setAttribute("notransition", "true"); 1.170 + } 1.171 + // Determine the CSS transform based on the next node: 1.172 + child.style.transform = this._getNextPos(child, shiftDown, aSize); 1.173 + } else { 1.174 + // If we're not shifting this node, reset the transform 1.175 + child.style.transform = ""; 1.176 + } 1.177 + } 1.178 + if (aContainer.lastChild && aIsFromThisArea && 1.179 + !this._lastPlaceholderInsertion) { 1.180 + // Flush layout: 1.181 + aContainer.lastChild.getBoundingClientRect(); 1.182 + // then remove all the [notransition] 1.183 + for (let child of aContainer.children) { 1.184 + child.removeAttribute("notransition"); 1.185 + } 1.186 + } 1.187 + delete this.__moveDown; 1.188 + delete this.__undoShift; 1.189 + this._lastPlaceholderInsertion = aBefore; 1.190 + }, 1.191 + 1.192 + isWide: function(aNode) { 1.193 + return this._wideCache.has(aNode.id); 1.194 + }, 1.195 + 1.196 + _checkIfWide: function(aNode) { 1.197 + return this._inPanel && aNode && aNode.firstChild && 1.198 + aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS); 1.199 + }, 1.200 + 1.201 + /** 1.202 + * Reset all the transforms in this container, optionally without 1.203 + * transitioning them. 1.204 + * @param aContainer the container in which to reset transforms 1.205 + * @param aNoTransition if truthy, adds a notransition attribute to the node 1.206 + * while resetting the transform. 1.207 + */ 1.208 + clearPlaceholders: function(aContainer, aNoTransition) { 1.209 + for (let child of aContainer.children) { 1.210 + if (aNoTransition) { 1.211 + child.setAttribute("notransition", true); 1.212 + } 1.213 + child.style.transform = ""; 1.214 + if (aNoTransition) { 1.215 + // Need to force a reflow otherwise this won't work. 1.216 + child.getBoundingClientRect(); 1.217 + child.removeAttribute("notransition"); 1.218 + } 1.219 + } 1.220 + // We snapped back, so we can assume there's no more 1.221 + // "last" placeholder insertion point to keep track of. 1.222 + if (aNoTransition) { 1.223 + this._lastPlaceholderInsertion = null; 1.224 + } 1.225 + }, 1.226 + 1.227 + _getNextPos: function(aNode, aShiftDown, aSize) { 1.228 + // Shifting down is easy: 1.229 + if (this._inPanel && aShiftDown) { 1.230 + return "translate(0, " + aSize.height + "px)"; 1.231 + } 1.232 + return this._diffWithNext(aNode, aSize); 1.233 + }, 1.234 + 1.235 + _diffWithNext: function(aNode, aSize) { 1.236 + let xDiff; 1.237 + let yDiff = null; 1.238 + let nodeBounds = this._lazyStoreGet(aNode); 1.239 + let side = this._dir == "ltr" ? "left" : "right"; 1.240 + let next = this._getVisibleSiblingForDirection(aNode, "next"); 1.241 + // First we determine the transform along the x axis. 1.242 + // Usually, there will be a next node to base this on: 1.243 + if (next) { 1.244 + let otherBounds = this._lazyStoreGet(next); 1.245 + xDiff = otherBounds[side] - nodeBounds[side]; 1.246 + // If the next node is a wide item in the panel, check if we could maybe 1.247 + // just move further out in the same row, without snapping to the next 1.248 + // one. This happens, for example, if moving an item that's before a wide 1.249 + // node within its own row of items. There will be space to drop this 1.250 + // item within the row, and the rest of the items do not need to shift. 1.251 + if (this.isWide(next)) { 1.252 + let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, 1.253 + this._firstInRow(aNode)); 1.254 + // If this has the same sign as our original shift, we're still 1.255 + // snapping to the start of the row. In this case, we should move 1.256 + // everything after us a row down, so as not to display two nodes on 1.257 + // top of each other: 1.258 + // (we would be able to get away with checking for equality instead of 1.259 + // equal signs here, but one of these is based on the x coordinate of 1.260 + // the first item in row N and one on that for row N - 1, so this is 1.261 + // safer, as their margins might differ) 1.262 + if ((otherXDiff < 0) == (xDiff < 0)) { 1.263 + this.__moveDown = true; 1.264 + } else { 1.265 + // Otherwise, we succeeded and can move further out. This also means 1.266 + // we can stop shifting the rest of the content: 1.267 + xDiff = otherXDiff; 1.268 + this.__undoShift = true; 1.269 + } 1.270 + } else { 1.271 + // We set this explicitly because otherwise some strange difference 1.272 + // between the height and the actual difference between line creeps in 1.273 + // and messes with alignments 1.274 + yDiff = otherBounds.top - nodeBounds.top; 1.275 + } 1.276 + } else { 1.277 + // We don't have a sibling whose position we can use. First, let's see 1.278 + // if we're also the first item (which complicates things): 1.279 + let firstNode = this._firstInRow(aNode); 1.280 + if (aNode == firstNode) { 1.281 + // Maybe we stored the horizontal distance between non-wide nodes, 1.282 + // if not, we'll use the width of the incoming node as a proxy: 1.283 + xDiff = this._horizontalDistance || aSize.width; 1.284 + } else { 1.285 + // If not, we should be able to get the distance to the previous node 1.286 + // and use the inverse, unless there's no room for another node (ie we 1.287 + // are the last node and there's no room for another one) 1.288 + xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode); 1.289 + } 1.290 + } 1.291 + 1.292 + // If we've not determined the vertical difference yet, check it here 1.293 + if (yDiff === null) { 1.294 + // If the next node is behind rather than in front, we must have moved 1.295 + // vertically: 1.296 + if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) { 1.297 + yDiff = aSize.height; 1.298 + } else { 1.299 + // Otherwise, we haven't 1.300 + yDiff = 0; 1.301 + } 1.302 + } 1.303 + return "translate(" + xDiff + "px, " + yDiff + "px)"; 1.304 + }, 1.305 + 1.306 + /** 1.307 + * Helper function to find the transform a node if there isn't a next node 1.308 + * to base that on. 1.309 + * @param aNode the node to transform 1.310 + * @param aNodeBounds the bounding rect info of this node 1.311 + * @param aFirstNodeInRow the first node in aNode's row 1.312 + */ 1.313 + _moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) { 1.314 + let next = this._getVisibleSiblingForDirection(aNode, "previous"); 1.315 + let otherBounds = this._lazyStoreGet(next); 1.316 + let side = this._dir == "ltr" ? "left" : "right"; 1.317 + let xDiff = aNodeBounds[side] - otherBounds[side]; 1.318 + // If, however, this means we move outside the container's box 1.319 + // (i.e. the row in which this item is placed is full) 1.320 + // we should move it to align with the first item in the next row instead 1.321 + let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"]; 1.322 + if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) || 1.323 + (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) { 1.324 + xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side]; 1.325 + } 1.326 + return xDiff; 1.327 + }, 1.328 + 1.329 + /** 1.330 + * Get position details from our cache. If the node is not yet cached, get its position 1.331 + * information and cache it now. 1.332 + * @param aNode the node whose position info we want 1.333 + * @return the position info 1.334 + */ 1.335 + _lazyStoreGet: function(aNode) { 1.336 + let rect = this._nodePositionStore.get(aNode); 1.337 + if (!rect) { 1.338 + // getBoundingClientRect() returns a DOMRect that is live, meaning that 1.339 + // as the element moves around, the rects values change. We don't want 1.340 + // that - we want a snapshot of what the rect values are right at this 1.341 + // moment, and nothing else. So we have to clone the values. 1.342 + let clientRect = aNode.getBoundingClientRect(); 1.343 + rect = { 1.344 + left: clientRect.left, 1.345 + right: clientRect.right, 1.346 + width: clientRect.width, 1.347 + height: clientRect.height, 1.348 + top: clientRect.top, 1.349 + bottom: clientRect.bottom, 1.350 + }; 1.351 + rect.x = rect.left + rect.width / 2; 1.352 + rect.y = rect.top + rect.height / 2; 1.353 + Object.freeze(rect); 1.354 + this._nodePositionStore.set(aNode, rect); 1.355 + } 1.356 + return rect; 1.357 + }, 1.358 + 1.359 + _firstInRow: function(aNode) { 1.360 + // XXXmconley: I'm not entirely sure why we need to take the floor of these 1.361 + // values - it looks like, periodically, we're getting fractional pixels back 1.362 + //from lazyStoreGet. I've filed bug 994247 to investigate. 1.363 + let bound = Math.floor(this._lazyStoreGet(aNode).top); 1.364 + let rv = aNode; 1.365 + let prev; 1.366 + while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) { 1.367 + if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) { 1.368 + return rv; 1.369 + } 1.370 + rv = prev; 1.371 + } 1.372 + return rv; 1.373 + }, 1.374 + 1.375 + _getVisibleSiblingForDirection: function(aNode, aDirection) { 1.376 + let rv = aNode; 1.377 + do { 1.378 + rv = rv[aDirection + "Sibling"]; 1.379 + } while (rv && rv.getAttribute("hidden") == "true") 1.380 + return rv; 1.381 + } 1.382 +} 1.383 + 1.384 +let DragPositionManager = { 1.385 + start: function(aWindow) { 1.386 + let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar"); 1.387 + areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow)); 1.388 + areas.push(aWindow.document.getElementById(kPaletteId)); 1.389 + for (let areaNode of areas) { 1.390 + let positionManager = gManagers.get(areaNode); 1.391 + if (positionManager) { 1.392 + positionManager.update(areaNode); 1.393 + } else { 1.394 + gManagers.set(areaNode, new AreaPositionManager(areaNode)); 1.395 + } 1.396 + } 1.397 + }, 1.398 + 1.399 + add: function(aWindow, aArea, aContainer) { 1.400 + if (CustomizableUI.getAreaType(aArea) != "toolbar") { 1.401 + return; 1.402 + } 1.403 + 1.404 + gManagers.set(aContainer, new AreaPositionManager(aContainer)); 1.405 + }, 1.406 + 1.407 + remove: function(aWindow, aArea, aContainer) { 1.408 + if (CustomizableUI.getAreaType(aArea) != "toolbar") { 1.409 + return; 1.410 + } 1.411 + 1.412 + gManagers.delete(aContainer); 1.413 + }, 1.414 + 1.415 + stop: function() { 1.416 + gManagers.clear(); 1.417 + }, 1.418 + 1.419 + getManagerForArea: function(aArea) { 1.420 + return gManagers.get(aArea); 1.421 + } 1.422 +}; 1.423 + 1.424 +Object.freeze(DragPositionManager); 1.425 +