Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
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/. */
5 "use strict";
7 Components.utils.import("resource:///modules/CustomizableUI.jsm");
9 let gManagers = new WeakMap();
11 const kPaletteId = "customization-palette";
12 const kPlaceholderClass = "panel-customization-placeholder";
14 this.EXPORTED_SYMBOLS = ["DragPositionManager"];
16 function AreaPositionManager(aContainer) {
17 // Caching the direction and bounds of the container for quick access later:
18 let window = aContainer.ownerDocument.defaultView;
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._inPanel = aContainer.id == CustomizableUI.AREA_PANEL;
28 this._horizontalDistance = null;
29 this.update(aContainer);
30 }
32 AreaPositionManager.prototype = {
33 _nodePositionStore: null,
34 _wideCache: null,
36 update: function(aContainer) {
37 let window = aContainer.ownerDocument.defaultView;
38 this._nodePositionStore = new WeakMap();
39 this._wideCache = new Set();
40 let last = null;
41 let singleItemHeight;
42 for (let child of aContainer.children) {
43 if (child.hidden) {
44 continue;
45 }
46 let isNodeWide = this._checkIfWide(child);
47 if (isNodeWide) {
48 this._wideCache.add(child.id);
49 }
50 let coordinates = this._lazyStoreGet(child);
51 // We keep a baseline horizontal distance between non-wide nodes around
52 // for use when we can't compare with previous/next nodes
53 if (!this._horizontalDistance && last && !isNodeWide) {
54 this._horizontalDistance = coordinates.left - last.left;
55 }
56 // We also keep the basic height of non-wide items for use below:
57 if (!isNodeWide && !singleItemHeight) {
58 singleItemHeight = coordinates.height;
59 }
60 last = !isNodeWide ? coordinates : null;
61 }
62 if (this._inPanel) {
63 this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT;
64 } else {
65 this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
66 }
67 },
69 /**
70 * Find the closest node in the container given the coordinates.
71 * "Closest" is defined in a somewhat strange manner: we prefer nodes
72 * which are in the same row over nodes that are in a different row.
73 * In order to implement this, we use a weighted cartesian distance
74 * where dy is more heavily weighted by a factor corresponding to the
75 * ratio between the container's width and the height of its elements.
76 */
77 find: function(aContainer, aX, aY, aDraggedItemId) {
78 let closest = null;
79 let minCartesian = Number.MAX_VALUE;
80 let containerX = this._containerInfo.left;
81 let containerY = this._containerInfo.top;
82 for (let node of aContainer.children) {
83 let coordinates = this._lazyStoreGet(node);
84 let offsetX = coordinates.x - containerX;
85 let offsetY = coordinates.y - containerY;
86 let hDiff = offsetX - aX;
87 let vDiff = offsetY - aY;
88 // For wide widgets, we're always going to be further from the center
89 // horizontally. Compensate:
90 if (this.isWide(node)) {
91 hDiff /= CustomizableUI.PANEL_COLUMN_COUNT;
92 }
93 // Then compensate for the height/width ratio so that we prefer items
94 // which are in the same row:
95 hDiff /= this._heightToWidthFactor;
97 let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
98 if (cartesianDiff < minCartesian) {
99 minCartesian = cartesianDiff;
100 closest = node;
101 }
102 }
104 // Now correct this node based on what we're dragging
105 if (closest) {
106 let doc = aContainer.ownerDocument;
107 let draggedItem = doc.getElementById(aDraggedItemId);
108 // If dragging a wide item, always pick the first item in a row:
109 if (this._inPanel && draggedItem &&
110 draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
111 return this._firstInRow(closest);
112 }
113 let targetBounds = this._lazyStoreGet(closest);
114 let farSide = this._dir == "ltr" ? "right" : "left";
115 let outsideX = targetBounds[farSide];
116 // Check if we're closer to the next target than to this one:
117 // Only move if we're not targeting a node in a different row:
118 if (aY > targetBounds.top && aY < targetBounds.bottom) {
119 if ((this._dir == "ltr" && aX > outsideX) ||
120 (this._dir == "rtl" && aX < outsideX)) {
121 return closest.nextSibling || aContainer;
122 }
123 }
124 }
125 return closest;
126 },
128 /**
129 * "Insert" a "placeholder" by shifting the subsequent children out of the
130 * way. We go through all the children, and shift them based on the position
131 * they would have if we had inserted something before aBefore. We use CSS
132 * transforms for this, which are CSS transitioned.
133 */
134 insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) {
135 let isShifted = false;
136 let shiftDown = aWide;
137 for (let child of aContainer.children) {
138 // Don't need to shift hidden nodes:
139 if (child.getAttribute("hidden") == "true") {
140 continue;
141 }
142 // If this is the node before which we're inserting, start shifting
143 // everything that comes after. One exception is inserting at the end
144 // of the menupanel, in which case we do not shift the placeholders:
145 if (child == aBefore && !child.classList.contains(kPlaceholderClass)) {
146 isShifted = true;
147 // If the node before which we're inserting is wide, we should
148 // shift everything one row down:
149 if (!shiftDown && this.isWide(child)) {
150 shiftDown = true;
151 }
152 }
153 // If we're moving items before a wide node that were already there,
154 // it's possible it's not necessary to shift nodes
155 // including & after the wide node.
156 if (this.__undoShift) {
157 isShifted = false;
158 }
159 if (isShifted) {
160 // Conversely, if we're adding something before a wide node, for
161 // simplicity's sake we move everything including the wide node down:
162 if (this.__moveDown) {
163 shiftDown = true;
164 }
165 if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
166 child.setAttribute("notransition", "true");
167 }
168 // Determine the CSS transform based on the next node:
169 child.style.transform = this._getNextPos(child, shiftDown, aSize);
170 } else {
171 // If we're not shifting this node, reset the transform
172 child.style.transform = "";
173 }
174 }
175 if (aContainer.lastChild && aIsFromThisArea &&
176 !this._lastPlaceholderInsertion) {
177 // Flush layout:
178 aContainer.lastChild.getBoundingClientRect();
179 // then remove all the [notransition]
180 for (let child of aContainer.children) {
181 child.removeAttribute("notransition");
182 }
183 }
184 delete this.__moveDown;
185 delete this.__undoShift;
186 this._lastPlaceholderInsertion = aBefore;
187 },
189 isWide: function(aNode) {
190 return this._wideCache.has(aNode.id);
191 },
193 _checkIfWide: function(aNode) {
194 return this._inPanel && aNode && aNode.firstChild &&
195 aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
196 },
198 /**
199 * Reset all the transforms in this container, optionally without
200 * transitioning them.
201 * @param aContainer the container in which to reset transforms
202 * @param aNoTransition if truthy, adds a notransition attribute to the node
203 * while resetting the transform.
204 */
205 clearPlaceholders: function(aContainer, aNoTransition) {
206 for (let child of aContainer.children) {
207 if (aNoTransition) {
208 child.setAttribute("notransition", true);
209 }
210 child.style.transform = "";
211 if (aNoTransition) {
212 // Need to force a reflow otherwise this won't work.
213 child.getBoundingClientRect();
214 child.removeAttribute("notransition");
215 }
216 }
217 // We snapped back, so we can assume there's no more
218 // "last" placeholder insertion point to keep track of.
219 if (aNoTransition) {
220 this._lastPlaceholderInsertion = null;
221 }
222 },
224 _getNextPos: function(aNode, aShiftDown, aSize) {
225 // Shifting down is easy:
226 if (this._inPanel && aShiftDown) {
227 return "translate(0, " + aSize.height + "px)";
228 }
229 return this._diffWithNext(aNode, aSize);
230 },
232 _diffWithNext: function(aNode, aSize) {
233 let xDiff;
234 let yDiff = null;
235 let nodeBounds = this._lazyStoreGet(aNode);
236 let side = this._dir == "ltr" ? "left" : "right";
237 let next = this._getVisibleSiblingForDirection(aNode, "next");
238 // First we determine the transform along the x axis.
239 // Usually, there will be a next node to base this on:
240 if (next) {
241 let otherBounds = this._lazyStoreGet(next);
242 xDiff = otherBounds[side] - nodeBounds[side];
243 // If the next node is a wide item in the panel, check if we could maybe
244 // just move further out in the same row, without snapping to the next
245 // one. This happens, for example, if moving an item that's before a wide
246 // node within its own row of items. There will be space to drop this
247 // item within the row, and the rest of the items do not need to shift.
248 if (this.isWide(next)) {
249 let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds,
250 this._firstInRow(aNode));
251 // If this has the same sign as our original shift, we're still
252 // snapping to the start of the row. In this case, we should move
253 // everything after us a row down, so as not to display two nodes on
254 // top of each other:
255 // (we would be able to get away with checking for equality instead of
256 // equal signs here, but one of these is based on the x coordinate of
257 // the first item in row N and one on that for row N - 1, so this is
258 // safer, as their margins might differ)
259 if ((otherXDiff < 0) == (xDiff < 0)) {
260 this.__moveDown = true;
261 } else {
262 // Otherwise, we succeeded and can move further out. This also means
263 // we can stop shifting the rest of the content:
264 xDiff = otherXDiff;
265 this.__undoShift = true;
266 }
267 } else {
268 // We set this explicitly because otherwise some strange difference
269 // between the height and the actual difference between line creeps in
270 // and messes with alignments
271 yDiff = otherBounds.top - nodeBounds.top;
272 }
273 } else {
274 // We don't have a sibling whose position we can use. First, let's see
275 // if we're also the first item (which complicates things):
276 let firstNode = this._firstInRow(aNode);
277 if (aNode == firstNode) {
278 // Maybe we stored the horizontal distance between non-wide nodes,
279 // if not, we'll use the width of the incoming node as a proxy:
280 xDiff = this._horizontalDistance || aSize.width;
281 } else {
282 // If not, we should be able to get the distance to the previous node
283 // and use the inverse, unless there's no room for another node (ie we
284 // are the last node and there's no room for another one)
285 xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
286 }
287 }
289 // If we've not determined the vertical difference yet, check it here
290 if (yDiff === null) {
291 // If the next node is behind rather than in front, we must have moved
292 // vertically:
293 if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) {
294 yDiff = aSize.height;
295 } else {
296 // Otherwise, we haven't
297 yDiff = 0;
298 }
299 }
300 return "translate(" + xDiff + "px, " + yDiff + "px)";
301 },
303 /**
304 * Helper function to find the transform a node if there isn't a next node
305 * to base that on.
306 * @param aNode the node to transform
307 * @param aNodeBounds the bounding rect info of this node
308 * @param aFirstNodeInRow the first node in aNode's row
309 */
310 _moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) {
311 let next = this._getVisibleSiblingForDirection(aNode, "previous");
312 let otherBounds = this._lazyStoreGet(next);
313 let side = this._dir == "ltr" ? "left" : "right";
314 let xDiff = aNodeBounds[side] - otherBounds[side];
315 // If, however, this means we move outside the container's box
316 // (i.e. the row in which this item is placed is full)
317 // we should move it to align with the first item in the next row instead
318 let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"];
319 if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) ||
320 (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) {
321 xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
322 }
323 return xDiff;
324 },
326 /**
327 * Get position details from our cache. If the node is not yet cached, get its position
328 * information and cache it now.
329 * @param aNode the node whose position info we want
330 * @return the position info
331 */
332 _lazyStoreGet: function(aNode) {
333 let rect = this._nodePositionStore.get(aNode);
334 if (!rect) {
335 // getBoundingClientRect() returns a DOMRect that is live, meaning that
336 // as the element moves around, the rects values change. We don't want
337 // that - we want a snapshot of what the rect values are right at this
338 // moment, and nothing else. So we have to clone the values.
339 let clientRect = aNode.getBoundingClientRect();
340 rect = {
341 left: clientRect.left,
342 right: clientRect.right,
343 width: clientRect.width,
344 height: clientRect.height,
345 top: clientRect.top,
346 bottom: clientRect.bottom,
347 };
348 rect.x = rect.left + rect.width / 2;
349 rect.y = rect.top + rect.height / 2;
350 Object.freeze(rect);
351 this._nodePositionStore.set(aNode, rect);
352 }
353 return rect;
354 },
356 _firstInRow: function(aNode) {
357 // XXXmconley: I'm not entirely sure why we need to take the floor of these
358 // values - it looks like, periodically, we're getting fractional pixels back
359 //from lazyStoreGet. I've filed bug 994247 to investigate.
360 let bound = Math.floor(this._lazyStoreGet(aNode).top);
361 let rv = aNode;
362 let prev;
363 while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
364 if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
365 return rv;
366 }
367 rv = prev;
368 }
369 return rv;
370 },
372 _getVisibleSiblingForDirection: function(aNode, aDirection) {
373 let rv = aNode;
374 do {
375 rv = rv[aDirection + "Sibling"];
376 } while (rv && rv.getAttribute("hidden") == "true")
377 return rv;
378 }
379 }
381 let DragPositionManager = {
382 start: function(aWindow) {
383 let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar");
384 areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow));
385 areas.push(aWindow.document.getElementById(kPaletteId));
386 for (let areaNode of areas) {
387 let positionManager = gManagers.get(areaNode);
388 if (positionManager) {
389 positionManager.update(areaNode);
390 } else {
391 gManagers.set(areaNode, new AreaPositionManager(areaNode));
392 }
393 }
394 },
396 add: function(aWindow, aArea, aContainer) {
397 if (CustomizableUI.getAreaType(aArea) != "toolbar") {
398 return;
399 }
401 gManagers.set(aContainer, new AreaPositionManager(aContainer));
402 },
404 remove: function(aWindow, aArea, aContainer) {
405 if (CustomizableUI.getAreaType(aArea) != "toolbar") {
406 return;
407 }
409 gManagers.delete(aContainer);
410 },
412 stop: function() {
413 gManagers.clear();
414 },
416 getManagerForArea: function(aArea) {
417 return gManagers.get(aArea);
418 }
419 };
421 Object.freeze(DragPositionManager);