|
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 |
|
7 Components.utils.import("resource:///modules/CustomizableUI.jsm"); |
|
8 |
|
9 let gManagers = new WeakMap(); |
|
10 |
|
11 const kPaletteId = "customization-palette"; |
|
12 const kPlaceholderClass = "panel-customization-placeholder"; |
|
13 |
|
14 this.EXPORTED_SYMBOLS = ["DragPositionManager"]; |
|
15 |
|
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 } |
|
31 |
|
32 AreaPositionManager.prototype = { |
|
33 _nodePositionStore: null, |
|
34 _wideCache: null, |
|
35 |
|
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 }, |
|
68 |
|
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; |
|
96 |
|
97 let cartesianDiff = hDiff * hDiff + vDiff * vDiff; |
|
98 if (cartesianDiff < minCartesian) { |
|
99 minCartesian = cartesianDiff; |
|
100 closest = node; |
|
101 } |
|
102 } |
|
103 |
|
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 }, |
|
127 |
|
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 }, |
|
188 |
|
189 isWide: function(aNode) { |
|
190 return this._wideCache.has(aNode.id); |
|
191 }, |
|
192 |
|
193 _checkIfWide: function(aNode) { |
|
194 return this._inPanel && aNode && aNode.firstChild && |
|
195 aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS); |
|
196 }, |
|
197 |
|
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 }, |
|
223 |
|
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 }, |
|
231 |
|
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 } |
|
288 |
|
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 }, |
|
302 |
|
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 }, |
|
325 |
|
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 }, |
|
355 |
|
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 }, |
|
371 |
|
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 } |
|
380 |
|
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 }, |
|
395 |
|
396 add: function(aWindow, aArea, aContainer) { |
|
397 if (CustomizableUI.getAreaType(aArea) != "toolbar") { |
|
398 return; |
|
399 } |
|
400 |
|
401 gManagers.set(aContainer, new AreaPositionManager(aContainer)); |
|
402 }, |
|
403 |
|
404 remove: function(aWindow, aArea, aContainer) { |
|
405 if (CustomizableUI.getAreaType(aArea) != "toolbar") { |
|
406 return; |
|
407 } |
|
408 |
|
409 gManagers.delete(aContainer); |
|
410 }, |
|
411 |
|
412 stop: function() { |
|
413 gManagers.clear(); |
|
414 }, |
|
415 |
|
416 getManagerForArea: function(aArea) { |
|
417 return gManagers.get(aArea); |
|
418 } |
|
419 }; |
|
420 |
|
421 Object.freeze(DragPositionManager); |
|
422 |