1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7ChromeUtils.import("resource:///modules/CustomizableUI.jsm"); 8ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 9 10var gManagers = new WeakMap(); 11 12const kPaletteId = "customization-palette"; 13 14var EXPORTED_SYMBOLS = ["DragPositionManager"]; 15 16function AreaPositionManager(aContainer) { 17 // Caching the direction and bounds of the container for quick access later: 18 let window = aContainer.ownerGlobal; 19 this._dir = window.getComputedStyle(aContainer).direction; 20 let containerRect = aContainer.getBoundingClientRect(); 21 this._containerInfo = { 22 left: containerRect.left, 23 right: containerRect.right, 24 top: containerRect.top, 25 width: containerRect.width 26 }; 27 this._horizontalDistance = null; 28 this.update(aContainer); 29} 30 31AreaPositionManager.prototype = { 32 _nodePositionStore: null, 33 34 update(aContainer) { 35 this._nodePositionStore = new WeakMap(); 36 let last = null; 37 let singleItemHeight; 38 for (let child of aContainer.children) { 39 if (child.hidden) { 40 continue; 41 } 42 let coordinates = this._lazyStoreGet(child); 43 // We keep a baseline horizontal distance between nodes around 44 // for use when we can't compare with previous/next nodes 45 if (!this._horizontalDistance && last) { 46 this._horizontalDistance = coordinates.left - last.left; 47 } 48 // We also keep the basic height of items for use below: 49 if (!singleItemHeight) { 50 singleItemHeight = coordinates.height; 51 } 52 last = coordinates; 53 } 54 this._heightToWidthFactor = this._containerInfo.width / singleItemHeight; 55 }, 56 57 /** 58 * Find the closest node in the container given the coordinates. 59 * "Closest" is defined in a somewhat strange manner: we prefer nodes 60 * which are in the same row over nodes that are in a different row. 61 * In order to implement this, we use a weighted cartesian distance 62 * where dy is more heavily weighted by a factor corresponding to the 63 * ratio between the container's width and the height of its elements. 64 */ 65 find(aContainer, aX, aY) { 66 let closest = null; 67 let minCartesian = Number.MAX_VALUE; 68 let containerX = this._containerInfo.left; 69 let containerY = this._containerInfo.top; 70 for (let node of aContainer.children) { 71 let coordinates = this._lazyStoreGet(node); 72 let offsetX = coordinates.x - containerX; 73 let offsetY = coordinates.y - containerY; 74 let hDiff = offsetX - aX; 75 let vDiff = offsetY - aY; 76 // Then compensate for the height/width ratio so that we prefer items 77 // which are in the same row: 78 hDiff /= this._heightToWidthFactor; 79 80 let cartesianDiff = hDiff * hDiff + vDiff * vDiff; 81 if (cartesianDiff < minCartesian) { 82 minCartesian = cartesianDiff; 83 closest = node; 84 } 85 } 86 87 // Now correct this node based on what we're dragging 88 if (closest) { 89 let targetBounds = this._lazyStoreGet(closest); 90 let farSide = this._dir == "ltr" ? "right" : "left"; 91 let outsideX = targetBounds[farSide]; 92 // Check if we're closer to the next target than to this one: 93 // Only move if we're not targeting a node in a different row: 94 if (aY > targetBounds.top && aY < targetBounds.bottom) { 95 if ((this._dir == "ltr" && aX > outsideX) || 96 (this._dir == "rtl" && aX < outsideX)) { 97 return closest.nextSibling || aContainer; 98 } 99 } 100 } 101 return closest; 102 }, 103 104 /** 105 * "Insert" a "placeholder" by shifting the subsequent children out of the 106 * way. We go through all the children, and shift them based on the position 107 * they would have if we had inserted something before aBefore. We use CSS 108 * transforms for this, which are CSS transitioned. 109 */ 110 insertPlaceholder(aContainer, aBefore, aSize, aIsFromThisArea) { 111 let isShifted = false; 112 for (let child of aContainer.children) { 113 // Don't need to shift hidden nodes: 114 if (child.getAttribute("hidden") == "true") { 115 continue; 116 } 117 // If this is the node before which we're inserting, start shifting 118 // everything that comes after. One exception is inserting at the end 119 // of the menupanel, in which case we do not shift the placeholders: 120 if (child == aBefore) { 121 isShifted = true; 122 } 123 if (isShifted) { 124 if (aIsFromThisArea && !this._lastPlaceholderInsertion) { 125 child.setAttribute("notransition", "true"); 126 } 127 // Determine the CSS transform based on the next node: 128 child.style.transform = this._diffWithNext(child, aSize); 129 } else { 130 // If we're not shifting this node, reset the transform 131 child.style.transform = ""; 132 } 133 } 134 if (aContainer.lastChild && aIsFromThisArea && 135 !this._lastPlaceholderInsertion) { 136 // Flush layout: 137 aContainer.lastChild.getBoundingClientRect(); 138 // then remove all the [notransition] 139 for (let child of aContainer.children) { 140 child.removeAttribute("notransition"); 141 } 142 } 143 this._lastPlaceholderInsertion = aBefore; 144 }, 145 146 /** 147 * Reset all the transforms in this container, optionally without 148 * transitioning them. 149 * @param aContainer the container in which to reset transforms 150 * @param aNoTransition if truthy, adds a notransition attribute to the node 151 * while resetting the transform. 152 */ 153 clearPlaceholders(aContainer, aNoTransition) { 154 for (let child of aContainer.children) { 155 if (aNoTransition) { 156 child.setAttribute("notransition", true); 157 } 158 child.style.transform = ""; 159 if (aNoTransition) { 160 // Need to force a reflow otherwise this won't work. 161 child.getBoundingClientRect(); 162 child.removeAttribute("notransition"); 163 } 164 } 165 // We snapped back, so we can assume there's no more 166 // "last" placeholder insertion point to keep track of. 167 if (aNoTransition) { 168 this._lastPlaceholderInsertion = null; 169 } 170 }, 171 172 _diffWithNext(aNode, aSize) { 173 let xDiff; 174 let yDiff = null; 175 let nodeBounds = this._lazyStoreGet(aNode); 176 let side = this._dir == "ltr" ? "left" : "right"; 177 let next = this._getVisibleSiblingForDirection(aNode, "next"); 178 // First we determine the transform along the x axis. 179 // Usually, there will be a next node to base this on: 180 if (next) { 181 let otherBounds = this._lazyStoreGet(next); 182 xDiff = otherBounds[side] - nodeBounds[side]; 183 // We set this explicitly because otherwise some strange difference 184 // between the height and the actual difference between line creeps in 185 // and messes with alignments 186 yDiff = otherBounds.top - nodeBounds.top; 187 } else { 188 // We don't have a sibling whose position we can use. First, let's see 189 // if we're also the first item (which complicates things): 190 let firstNode = this._firstInRow(aNode); 191 if (aNode == firstNode) { 192 // Maybe we stored the horizontal distance between nodes, 193 // if not, we'll use the width of the incoming node as a proxy: 194 xDiff = this._horizontalDistance || (this._dir == "ltr" ? 1 : -1) * aSize.width; 195 } else { 196 // If not, we should be able to get the distance to the previous node 197 // and use the inverse, unless there's no room for another node (ie we 198 // are the last node and there's no room for another one) 199 xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode); 200 } 201 } 202 203 // If we've not determined the vertical difference yet, check it here 204 if (yDiff === null) { 205 // If the next node is behind rather than in front, we must have moved 206 // vertically: 207 if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) { 208 yDiff = aSize.height; 209 } else { 210 // Otherwise, we haven't 211 yDiff = 0; 212 } 213 } 214 return "translate(" + xDiff + "px, " + yDiff + "px)"; 215 }, 216 217 /** 218 * Helper function to find the transform a node if there isn't a next node 219 * to base that on. 220 * @param aNode the node to transform 221 * @param aNodeBounds the bounding rect info of this node 222 * @param aFirstNodeInRow the first node in aNode's row 223 */ 224 _moveNextBasedOnPrevious(aNode, aNodeBounds, aFirstNodeInRow) { 225 let next = this._getVisibleSiblingForDirection(aNode, "previous"); 226 let otherBounds = this._lazyStoreGet(next); 227 let side = this._dir == "ltr" ? "left" : "right"; 228 let xDiff = aNodeBounds[side] - otherBounds[side]; 229 // If, however, this means we move outside the container's box 230 // (i.e. the row in which this item is placed is full) 231 // we should move it to align with the first item in the next row instead 232 let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"]; 233 if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) || 234 (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) { 235 xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side]; 236 } 237 return xDiff; 238 }, 239 240 /** 241 * Get position details from our cache. If the node is not yet cached, get its position 242 * information and cache it now. 243 * @param aNode the node whose position info we want 244 * @return the position info 245 */ 246 _lazyStoreGet(aNode) { 247 let rect = this._nodePositionStore.get(aNode); 248 if (!rect) { 249 // getBoundingClientRect() returns a DOMRect that is live, meaning that 250 // as the element moves around, the rects values change. We don't want 251 // that - we want a snapshot of what the rect values are right at this 252 // moment, and nothing else. So we have to clone the values. 253 let clientRect = aNode.getBoundingClientRect(); 254 rect = { 255 left: clientRect.left, 256 right: clientRect.right, 257 width: clientRect.width, 258 height: clientRect.height, 259 top: clientRect.top, 260 bottom: clientRect.bottom, 261 }; 262 rect.x = rect.left + rect.width / 2; 263 rect.y = rect.top + rect.height / 2; 264 Object.freeze(rect); 265 this._nodePositionStore.set(aNode, rect); 266 } 267 return rect; 268 }, 269 270 _firstInRow(aNode) { 271 // XXXmconley: I'm not entirely sure why we need to take the floor of these 272 // values - it looks like, periodically, we're getting fractional pixels back 273 // from lazyStoreGet. I've filed bug 994247 to investigate. 274 let bound = Math.floor(this._lazyStoreGet(aNode).top); 275 let rv = aNode; 276 let prev; 277 while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) { 278 if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) { 279 return rv; 280 } 281 rv = prev; 282 } 283 return rv; 284 }, 285 286 _getVisibleSiblingForDirection(aNode, aDirection) { 287 let rv = aNode; 288 do { 289 rv = rv[aDirection + "Sibling"]; 290 } while (rv && rv.getAttribute("hidden") == "true"); 291 return rv; 292 } 293}; 294 295var DragPositionManager = { 296 start(aWindow) { 297 let areas = [aWindow.document.getElementById(kPaletteId)]; 298 for (let areaNode of areas) { 299 let positionManager = gManagers.get(areaNode); 300 if (positionManager) { 301 positionManager.update(areaNode); 302 } else { 303 gManagers.set(areaNode, new AreaPositionManager(areaNode)); 304 } 305 } 306 }, 307 308 stop() { 309 gManagers = new WeakMap(); 310 }, 311 312 getManagerForArea(aArea) { 313 return gManagers.get(aArea); 314 } 315}; 316 317Object.freeze(DragPositionManager); 318