forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			317 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			317 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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";
 | 
						|
 | 
						|
ChromeUtils.import("resource:///modules/CustomizableUI.jsm");
 | 
						|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 | 
						|
 | 
						|
var gManagers = new WeakMap();
 | 
						|
 | 
						|
const kPaletteId = "customization-palette";
 | 
						|
 | 
						|
var EXPORTED_SYMBOLS = ["DragPositionManager"];
 | 
						|
 | 
						|
function AreaPositionManager(aContainer) {
 | 
						|
  // Caching the direction and bounds of the container for quick access later:
 | 
						|
  let window = aContainer.ownerGlobal;
 | 
						|
  this._dir = window.getComputedStyle(aContainer).direction;
 | 
						|
  let containerRect = aContainer.getBoundingClientRect();
 | 
						|
  this._containerInfo = {
 | 
						|
    left: containerRect.left,
 | 
						|
    right: containerRect.right,
 | 
						|
    top: containerRect.top,
 | 
						|
    width: containerRect.width
 | 
						|
  };
 | 
						|
  this._horizontalDistance = null;
 | 
						|
  this.update(aContainer);
 | 
						|
}
 | 
						|
 | 
						|
AreaPositionManager.prototype = {
 | 
						|
  _nodePositionStore: null,
 | 
						|
 | 
						|
  update(aContainer) {
 | 
						|
    this._nodePositionStore = new WeakMap();
 | 
						|
    let last = null;
 | 
						|
    let singleItemHeight;
 | 
						|
    for (let child of aContainer.children) {
 | 
						|
      if (child.hidden) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      let coordinates = this._lazyStoreGet(child);
 | 
						|
      // We keep a baseline horizontal distance between nodes around
 | 
						|
      // for use when we can't compare with previous/next nodes
 | 
						|
      if (!this._horizontalDistance && last) {
 | 
						|
        this._horizontalDistance = coordinates.left - last.left;
 | 
						|
      }
 | 
						|
      // We also keep the basic height of items for use below:
 | 
						|
      if (!singleItemHeight) {
 | 
						|
        singleItemHeight = coordinates.height;
 | 
						|
      }
 | 
						|
      last = coordinates;
 | 
						|
    }
 | 
						|
    this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find the closest node in the container given the coordinates.
 | 
						|
   * "Closest" is defined in a somewhat strange manner: we prefer nodes
 | 
						|
   * which are in the same row over nodes that are in a different row.
 | 
						|
   * In order to implement this, we use a weighted cartesian distance
 | 
						|
   * where dy is more heavily weighted by a factor corresponding to the
 | 
						|
   * ratio between the container's width and the height of its elements.
 | 
						|
   */
 | 
						|
  find(aContainer, aX, aY) {
 | 
						|
    let closest = null;
 | 
						|
    let minCartesian = Number.MAX_VALUE;
 | 
						|
    let containerX = this._containerInfo.left;
 | 
						|
    let containerY = this._containerInfo.top;
 | 
						|
    for (let node of aContainer.children) {
 | 
						|
      let coordinates = this._lazyStoreGet(node);
 | 
						|
      let offsetX = coordinates.x - containerX;
 | 
						|
      let offsetY = coordinates.y - containerY;
 | 
						|
      let hDiff = offsetX - aX;
 | 
						|
      let vDiff = offsetY - aY;
 | 
						|
      // Then compensate for the height/width ratio so that we prefer items
 | 
						|
      // which are in the same row:
 | 
						|
      hDiff /= this._heightToWidthFactor;
 | 
						|
 | 
						|
      let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
 | 
						|
      if (cartesianDiff < minCartesian) {
 | 
						|
        minCartesian = cartesianDiff;
 | 
						|
        closest = node;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Now correct this node based on what we're dragging
 | 
						|
    if (closest) {
 | 
						|
      let targetBounds = this._lazyStoreGet(closest);
 | 
						|
      let farSide = this._dir == "ltr" ? "right" : "left";
 | 
						|
      let outsideX = targetBounds[farSide];
 | 
						|
      // Check if we're closer to the next target than to this one:
 | 
						|
      // Only move if we're not targeting a node in a different row:
 | 
						|
      if (aY > targetBounds.top && aY < targetBounds.bottom) {
 | 
						|
        if ((this._dir == "ltr" && aX > outsideX) ||
 | 
						|
            (this._dir == "rtl" && aX < outsideX)) {
 | 
						|
          return closest.nextSibling || aContainer;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return closest;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * "Insert" a "placeholder" by shifting the subsequent children out of the
 | 
						|
   * way. We go through all the children, and shift them based on the position
 | 
						|
   * they would have if we had inserted something before aBefore. We use CSS
 | 
						|
   * transforms for this, which are CSS transitioned.
 | 
						|
   */
 | 
						|
  insertPlaceholder(aContainer, aBefore, aSize, aIsFromThisArea) {
 | 
						|
    let isShifted = false;
 | 
						|
    for (let child of aContainer.children) {
 | 
						|
      // Don't need to shift hidden nodes:
 | 
						|
      if (child.getAttribute("hidden") == "true") {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      // If this is the node before which we're inserting, start shifting
 | 
						|
      // everything that comes after. One exception is inserting at the end
 | 
						|
      // of the menupanel, in which case we do not shift the placeholders:
 | 
						|
      if (child == aBefore) {
 | 
						|
        isShifted = true;
 | 
						|
      }
 | 
						|
      if (isShifted) {
 | 
						|
        if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
 | 
						|
          child.setAttribute("notransition", "true");
 | 
						|
        }
 | 
						|
        // Determine the CSS transform based on the next node:
 | 
						|
        child.style.transform = this._diffWithNext(child, aSize);
 | 
						|
      } else {
 | 
						|
        // If we're not shifting this node, reset the transform
 | 
						|
        child.style.transform = "";
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (aContainer.lastChild && aIsFromThisArea &&
 | 
						|
        !this._lastPlaceholderInsertion) {
 | 
						|
      // Flush layout:
 | 
						|
      aContainer.lastChild.getBoundingClientRect();
 | 
						|
      // then remove all the [notransition]
 | 
						|
      for (let child of aContainer.children) {
 | 
						|
        child.removeAttribute("notransition");
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this._lastPlaceholderInsertion = aBefore;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Reset all the transforms in this container, optionally without
 | 
						|
   * transitioning them.
 | 
						|
   * @param aContainer    the container in which to reset transforms
 | 
						|
   * @param aNoTransition if truthy, adds a notransition attribute to the node
 | 
						|
   *                      while resetting the transform.
 | 
						|
   */
 | 
						|
  clearPlaceholders(aContainer, aNoTransition) {
 | 
						|
    for (let child of aContainer.children) {
 | 
						|
      if (aNoTransition) {
 | 
						|
        child.setAttribute("notransition", true);
 | 
						|
      }
 | 
						|
      child.style.transform = "";
 | 
						|
      if (aNoTransition) {
 | 
						|
        // Need to force a reflow otherwise this won't work.
 | 
						|
        child.getBoundingClientRect();
 | 
						|
        child.removeAttribute("notransition");
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // We snapped back, so we can assume there's no more
 | 
						|
    // "last" placeholder insertion point to keep track of.
 | 
						|
    if (aNoTransition) {
 | 
						|
      this._lastPlaceholderInsertion = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _diffWithNext(aNode, aSize) {
 | 
						|
    let xDiff;
 | 
						|
    let yDiff = null;
 | 
						|
    let nodeBounds = this._lazyStoreGet(aNode);
 | 
						|
    let side = this._dir == "ltr" ? "left" : "right";
 | 
						|
    let next = this._getVisibleSiblingForDirection(aNode, "next");
 | 
						|
    // First we determine the transform along the x axis.
 | 
						|
    // Usually, there will be a next node to base this on:
 | 
						|
    if (next) {
 | 
						|
      let otherBounds = this._lazyStoreGet(next);
 | 
						|
      xDiff = otherBounds[side] - nodeBounds[side];
 | 
						|
      // We set this explicitly because otherwise some strange difference
 | 
						|
      // between the height and the actual difference between line creeps in
 | 
						|
      // and messes with alignments
 | 
						|
      yDiff = otherBounds.top - nodeBounds.top;
 | 
						|
    } else {
 | 
						|
      // We don't have a sibling whose position we can use. First, let's see
 | 
						|
      // if we're also the first item (which complicates things):
 | 
						|
      let firstNode = this._firstInRow(aNode);
 | 
						|
      if (aNode == firstNode) {
 | 
						|
        // Maybe we stored the horizontal distance between nodes,
 | 
						|
        // if not, we'll use the width of the incoming node as a proxy:
 | 
						|
        xDiff = this._horizontalDistance || (this._dir == "ltr" ? 1 : -1) * aSize.width;
 | 
						|
      } else {
 | 
						|
        // If not, we should be able to get the distance to the previous node
 | 
						|
        // and use the inverse, unless there's no room for another node (ie we
 | 
						|
        // are the last node and there's no room for another one)
 | 
						|
        xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If we've not determined the vertical difference yet, check it here
 | 
						|
    if (yDiff === null) {
 | 
						|
      // If the next node is behind rather than in front, we must have moved
 | 
						|
      // vertically:
 | 
						|
      if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) {
 | 
						|
        yDiff = aSize.height;
 | 
						|
      } else {
 | 
						|
        // Otherwise, we haven't
 | 
						|
        yDiff = 0;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return "translate(" + xDiff + "px, " + yDiff + "px)";
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper function to find the transform a node if there isn't a next node
 | 
						|
   * to base that on.
 | 
						|
   * @param aNode           the node to transform
 | 
						|
   * @param aNodeBounds     the bounding rect info of this node
 | 
						|
   * @param aFirstNodeInRow the first node in aNode's row
 | 
						|
   */
 | 
						|
  _moveNextBasedOnPrevious(aNode, aNodeBounds, aFirstNodeInRow) {
 | 
						|
    let next = this._getVisibleSiblingForDirection(aNode, "previous");
 | 
						|
    let otherBounds = this._lazyStoreGet(next);
 | 
						|
    let side = this._dir == "ltr" ? "left" : "right";
 | 
						|
    let xDiff = aNodeBounds[side] - otherBounds[side];
 | 
						|
    // If, however, this means we move outside the container's box
 | 
						|
    // (i.e. the row in which this item is placed is full)
 | 
						|
    // we should move it to align with the first item in the next row instead
 | 
						|
    let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"];
 | 
						|
    if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) ||
 | 
						|
        (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) {
 | 
						|
      xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
 | 
						|
    }
 | 
						|
    return xDiff;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get position details from our cache. If the node is not yet cached, get its position
 | 
						|
   * information and cache it now.
 | 
						|
   * @param aNode  the node whose position info we want
 | 
						|
   * @return the position info
 | 
						|
   */
 | 
						|
  _lazyStoreGet(aNode) {
 | 
						|
    let rect = this._nodePositionStore.get(aNode);
 | 
						|
    if (!rect) {
 | 
						|
      // getBoundingClientRect() returns a DOMRect that is live, meaning that
 | 
						|
      // as the element moves around, the rects values change. We don't want
 | 
						|
      // that - we want a snapshot of what the rect values are right at this
 | 
						|
      // moment, and nothing else. So we have to clone the values.
 | 
						|
      let clientRect = aNode.getBoundingClientRect();
 | 
						|
      rect = {
 | 
						|
        left: clientRect.left,
 | 
						|
        right: clientRect.right,
 | 
						|
        width: clientRect.width,
 | 
						|
        height: clientRect.height,
 | 
						|
        top: clientRect.top,
 | 
						|
        bottom: clientRect.bottom,
 | 
						|
      };
 | 
						|
      rect.x = rect.left + rect.width / 2;
 | 
						|
      rect.y = rect.top + rect.height / 2;
 | 
						|
      Object.freeze(rect);
 | 
						|
      this._nodePositionStore.set(aNode, rect);
 | 
						|
    }
 | 
						|
    return rect;
 | 
						|
  },
 | 
						|
 | 
						|
  _firstInRow(aNode) {
 | 
						|
    // XXXmconley: I'm not entirely sure why we need to take the floor of these
 | 
						|
    // values - it looks like, periodically, we're getting fractional pixels back
 | 
						|
    // from lazyStoreGet. I've filed bug 994247 to investigate.
 | 
						|
    let bound = Math.floor(this._lazyStoreGet(aNode).top);
 | 
						|
    let rv = aNode;
 | 
						|
    let prev;
 | 
						|
    while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
 | 
						|
      if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
 | 
						|
        return rv;
 | 
						|
      }
 | 
						|
      rv = prev;
 | 
						|
    }
 | 
						|
    return rv;
 | 
						|
  },
 | 
						|
 | 
						|
  _getVisibleSiblingForDirection(aNode, aDirection) {
 | 
						|
    let rv = aNode;
 | 
						|
    do {
 | 
						|
      rv = rv[aDirection + "Sibling"];
 | 
						|
    } while (rv && rv.getAttribute("hidden") == "true");
 | 
						|
    return rv;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
var DragPositionManager = {
 | 
						|
  start(aWindow) {
 | 
						|
    let areas = [aWindow.document.getElementById(kPaletteId)];
 | 
						|
    for (let areaNode of areas) {
 | 
						|
      let positionManager = gManagers.get(areaNode);
 | 
						|
      if (positionManager) {
 | 
						|
        positionManager.update(areaNode);
 | 
						|
      } else {
 | 
						|
        gManagers.set(areaNode, new AreaPositionManager(areaNode));
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  stop() {
 | 
						|
    gManagers = new WeakMap();
 | 
						|
  },
 | 
						|
 | 
						|
  getManagerForArea(aArea) {
 | 
						|
    return gManagers.get(aArea);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
Object.freeze(DragPositionManager);
 |