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