Source code

Revision control

Other Tools

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
var gManagers = new WeakMap();
8
9
const kPaletteId = "customization-palette";
10
11
var EXPORTED_SYMBOLS = ["DragPositionManager"];
12
13
function AreaPositionManager(aContainer) {
14
// Caching the direction and bounds of the container for quick access later:
15
this._rtl = aContainer.ownerGlobal.RTL_UI;
16
let containerRect = aContainer.getBoundingClientRect();
17
this._containerInfo = {
18
left: containerRect.left,
19
right: containerRect.right,
20
top: containerRect.top,
21
width: containerRect.width,
22
};
23
this._horizontalDistance = null;
24
this.update(aContainer);
25
}
26
27
AreaPositionManager.prototype = {
28
_nodePositionStore: null,
29
30
update(aContainer) {
31
this._nodePositionStore = new WeakMap();
32
let last = null;
33
let singleItemHeight;
34
for (let child of aContainer.children) {
35
if (child.hidden) {
36
continue;
37
}
38
let coordinates = this._lazyStoreGet(child);
39
// We keep a baseline horizontal distance between nodes around
40
// for use when we can't compare with previous/next nodes
41
if (!this._horizontalDistance && last) {
42
this._horizontalDistance = coordinates.left - last.left;
43
}
44
// We also keep the basic height of items for use below:
45
if (!singleItemHeight) {
46
singleItemHeight = coordinates.height;
47
}
48
last = coordinates;
49
}
50
this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
51
},
52
53
/**
54
* Find the closest node in the container given the coordinates.
55
* "Closest" is defined in a somewhat strange manner: we prefer nodes
56
* which are in the same row over nodes that are in a different row.
57
* In order to implement this, we use a weighted cartesian distance
58
* where dy is more heavily weighted by a factor corresponding to the
59
* ratio between the container's width and the height of its elements.
60
*/
61
find(aContainer, aX, aY) {
62
let closest = null;
63
let minCartesian = Number.MAX_VALUE;
64
let containerX = this._containerInfo.left;
65
let containerY = this._containerInfo.top;
66
for (let node of aContainer.children) {
67
let coordinates = this._lazyStoreGet(node);
68
let offsetX = coordinates.x - containerX;
69
let offsetY = coordinates.y - containerY;
70
let hDiff = offsetX - aX;
71
let vDiff = offsetY - aY;
72
// Then compensate for the height/width ratio so that we prefer items
73
// which are in the same row:
74
hDiff /= this._heightToWidthFactor;
75
76
let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
77
if (cartesianDiff < minCartesian) {
78
minCartesian = cartesianDiff;
79
closest = node;
80
}
81
}
82
83
// Now correct this node based on what we're dragging
84
if (closest) {
85
let targetBounds = this._lazyStoreGet(closest);
86
let farSide = this._rtl ? "left" : "right";
87
let outsideX = targetBounds[farSide];
88
// Check if we're closer to the next target than to this one:
89
// Only move if we're not targeting a node in a different row:
90
if (aY > targetBounds.top && aY < targetBounds.bottom) {
91
if ((!this._rtl && aX > outsideX) || (this._rtl && aX < outsideX)) {
92
return closest.nextElementSibling || aContainer;
93
}
94
}
95
}
96
return closest;
97
},
98
99
/**
100
* "Insert" a "placeholder" by shifting the subsequent children out of the
101
* way. We go through all the children, and shift them based on the position
102
* they would have if we had inserted something before aBefore. We use CSS
103
* transforms for this, which are CSS transitioned.
104
*/
105
insertPlaceholder(aContainer, aBefore, aSize, aIsFromThisArea) {
106
let isShifted = false;
107
for (let child of aContainer.children) {
108
// Don't need to shift hidden nodes:
109
if (child.getAttribute("hidden") == "true") {
110
continue;
111
}
112
// If this is the node before which we're inserting, start shifting
113
// everything that comes after. One exception is inserting at the end
114
// of the menupanel, in which case we do not shift the placeholders:
115
if (child == aBefore) {
116
isShifted = true;
117
}
118
if (isShifted) {
119
if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
120
child.setAttribute("notransition", "true");
121
}
122
// Determine the CSS transform based on the next node:
123
child.style.transform = this._diffWithNext(child, aSize);
124
} else {
125
// If we're not shifting this node, reset the transform
126
child.style.transform = "";
127
}
128
}
129
if (
130
aContainer.lastElementChild &&
131
aIsFromThisArea &&
132
!this._lastPlaceholderInsertion
133
) {
134
// Flush layout:
135
aContainer.lastElementChild.getBoundingClientRect();
136
// then remove all the [notransition]
137
for (let child of aContainer.children) {
138
child.removeAttribute("notransition");
139
}
140
}
141
this._lastPlaceholderInsertion = aBefore;
142
},
143
144
/**
145
* Reset all the transforms in this container, optionally without
146
* transitioning them.
147
* @param aContainer the container in which to reset transforms
148
* @param aNoTransition if truthy, adds a notransition attribute to the node
149
* while resetting the transform.
150
*/
151
clearPlaceholders(aContainer, aNoTransition) {
152
for (let child of aContainer.children) {
153
if (aNoTransition) {
154
child.setAttribute("notransition", true);
155
}
156
child.style.transform = "";
157
if (aNoTransition) {
158
// Need to force a reflow otherwise this won't work.
159
child.getBoundingClientRect();
160
child.removeAttribute("notransition");
161
}
162
}
163
// We snapped back, so we can assume there's no more
164
// "last" placeholder insertion point to keep track of.
165
if (aNoTransition) {
166
this._lastPlaceholderInsertion = null;
167
}
168
},
169
170
_diffWithNext(aNode, aSize) {
171
let xDiff;
172
let yDiff = null;
173
let nodeBounds = this._lazyStoreGet(aNode);
174
let side = this._rtl ? "right" : "left";
175
let next = this._getVisibleSiblingForDirection(aNode, "next");
176
// First we determine the transform along the x axis.
177
// Usually, there will be a next node to base this on:
178
if (next) {
179
let otherBounds = this._lazyStoreGet(next);
180
xDiff = otherBounds[side] - nodeBounds[side];
181
// We set this explicitly because otherwise some strange difference
182
// between the height and the actual difference between line creeps in
183
// and messes with alignments
184
yDiff = otherBounds.top - nodeBounds.top;
185
} else {
186
// We don't have a sibling whose position we can use. First, let's see
187
// if we're also the first item (which complicates things):
188
let firstNode = this._firstInRow(aNode);
189
if (aNode == firstNode) {
190
// Maybe we stored the horizontal distance between nodes,
191
// if not, we'll use the width of the incoming node as a proxy:
192
xDiff = this._horizontalDistance || (this._rtl ? -1 : 1) * aSize.width;
193
} else {
194
// If not, we should be able to get the distance to the previous node
195
// and use the inverse, unless there's no room for another node (ie we
196
// are the last node and there's no room for another one)
197
xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
198
}
199
}
200
201
// If we've not determined the vertical difference yet, check it here
202
if (yDiff === null) {
203
// If the next node is behind rather than in front, we must have moved
204
// vertically:
205
if ((xDiff > 0 && this._rtl) || (xDiff < 0 && !this._rtl)) {
206
yDiff = aSize.height;
207
} else {
208
// Otherwise, we haven't
209
yDiff = 0;
210
}
211
}
212
return "translate(" + xDiff + "px, " + yDiff + "px)";
213
},
214
215
/**
216
* Helper function to find the transform a node if there isn't a next node
217
* to base that on.
218
* @param aNode the node to transform
219
* @param aNodeBounds the bounding rect info of this node
220
* @param aFirstNodeInRow the first node in aNode's row
221
*/
222
_moveNextBasedOnPrevious(aNode, aNodeBounds, aFirstNodeInRow) {
223
let next = this._getVisibleSiblingForDirection(aNode, "previous");
224
let otherBounds = this._lazyStoreGet(next);
225
let side = this._rtl ? "right" : "left";
226
let xDiff = aNodeBounds[side] - otherBounds[side];
227
// If, however, this means we move outside the container's box
228
// (i.e. the row in which this item is placed is full)
229
// we should move it to align with the first item in the next row instead
230
let bound = this._containerInfo[this._rtl ? "left" : "right"];
231
if (
232
(!this._rtl && xDiff + aNodeBounds.right > bound) ||
233
(this._rtl && xDiff + aNodeBounds.left < bound)
234
) {
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 + "ElementSibling"];
290
} while (rv && rv.getAttribute("hidden") == "true");
291
return rv;
292
},
293
};
294
295
var 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
317
Object.freeze(DragPositionManager);