browser/components/customizableui/src/DragPositionManager.jsm

changeset 0
6474c204b198
     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 +

mercurial