diff options
Diffstat (limited to 'browser/components/customizableui/DragPositionManager.sys.mjs')
-rw-r--r-- | browser/components/customizableui/DragPositionManager.sys.mjs | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/browser/components/customizableui/DragPositionManager.sys.mjs b/browser/components/customizableui/DragPositionManager.sys.mjs new file mode 100644 index 0000000000..dede3f94b1 --- /dev/null +++ b/browser/components/customizableui/DragPositionManager.sys.mjs @@ -0,0 +1,313 @@ +/* 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/. */ + +var gManagers = new WeakMap(); + +const kPaletteId = "customization-palette"; + +function AreaPositionManager(aContainer) { + // Caching the direction and bounds of the container for quick access later: + this._rtl = aContainer.ownerGlobal.RTL_UI; + 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._rtl ? "left" : "right"; + 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._rtl && aX > outsideX) || (this._rtl && aX < outsideX)) { + return closest.nextElementSibling || 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.hidden) { + 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.lastElementChild && + aIsFromThisArea && + !this._lastPlaceholderInsertion + ) { + // Flush layout: + aContainer.lastElementChild.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._rtl ? "right" : "left"; + 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._rtl ? -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._rtl) || (xDiff < 0 && !this._rtl)) { + 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._rtl ? "right" : "left"; + 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._rtl ? "left" : "right"]; + if ( + (!this._rtl && xDiff + aNodeBounds.right > bound) || + (this._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 + "ElementSibling"]; + } while (rv && rv.hidden); + return rv; + }, +}; + +export 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); |