File: DragPositionManager.sys.mjs

package info (click to toggle)
firefox 141.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,550,588 kB
  • sloc: cpp: 7,426,506; javascript: 6,367,238; ansic: 3,707,351; python: 1,369,002; xml: 623,983; asm: 426,918; java: 184,324; sh: 64,488; makefile: 19,203; objc: 13,059; perl: 12,955; yacc: 4,583; cs: 3,846; pascal: 3,352; lex: 1,720; ruby: 1,071; exp: 762; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10
file content (476 lines) | stat: -rw-r--r-- 17,378 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
/* 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/. */

/**
 * A cache of AreaPositionManagers weakly mapped to customization area nodes.
 *
 * @type {WeakMap<DOMNode, AreaPositionManager>}
 */
var gManagers = new WeakMap();

const kPaletteId = "customization-palette";

/**
 * An AreaPositionManager is used to power the animated drag-and-drop grid
 * behaviour of each customizable area (toolbars, the palette, the overflow
 * panel) while in customize mode. Each customizable area has its own
 * AreaPositionManager, per browser window.
 */
class AreaPositionManager {
  /**
   * True if the container is oriented from right-to-left.
   *
   * @type {boolean}
   */
  #rtl = false;

  /**
   * A DOMRectReadOnly for the bounding client rect for the container,
   * collected once during construction.
   *
   * @type {DOMRectReadOnly|null}
   */
  #containerInfo = null;

  /**
   * The calculated horizontal distance between the first two visible child
   * nodes of the container.
   *
   * @type {number}
   */
  #horizontalDistance = 0;

  /**
   * The ratio of the width of the container and the height of the first
   * visible child node. This is used in the weighted cartesian distance
   * calculation used in the AreaPositionManager.find method.
   *
   * @see AreaPositionManager.find
   * @type {number}
   */
  #heightToWidthFactor = 0;

  /**
   * Constructs an instance of AreaPositionManager for a customizable area.
   *
   * @param {DOMNode} aContainer
   *   The customizable area container node for which drag-and-drop animations
   *   are to be calculated for.
   */
  constructor(aContainer) {
    // Caching the direction and bounds of the container for quick access later:
    this.#rtl = aContainer.ownerGlobal.RTL_UI;
    this.#containerInfo = DOMRectReadOnly.fromRect(
      aContainer.getBoundingClientRect()
    );
    this.update(aContainer);
  }

  /**
   * A cache of container child node size and position data.
   *
   * @type {WeakMap<DOMNode, DOMRectReadOnly>}
   */
  #nodePositionStore = new WeakMap();

  /**
   * The child node immediately after the most recently placed placeholder. May
   * be null if no placeholder has been inserted yet, or if the placeholder is
   * at the end of the container.
   *
   * @type {DOMNode|null}
   */
  #lastPlaceholderInsertion = null;

  /**
   * Iterates the visible children of the container, sampling their bounding
   * client rects and storing them in a local cache. Also collects and stores
   * metrics like the horizontal distance between the first two children,
   * the height of the first item, and a ratio between the width of the
   * container and the height of the first child item.
   *
   * @param {DOMNode} aContainer
   *   The container node to collect the measurements for.
   */
  update(aContainer) {
    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.
   *
   * @param {DOMNode} aContainer
   *   The container element that contains one or more rows of child elements
   *   in some kind of grid formation.
   * @param {number} aX
   *   The X horizontal coordinate that we're finding the closest child node
   *   for.
   * @param {number} aY
   *   The Y vertical coordinate that we're finding the closest child node
   *   for.
   * @returns {DOMNode|null}
   *   The closest node to the aX and aY coordinates, preferring child nodes
   *   in the same row of the grid. This may also return the container itself,
   *   if the coordinates are on the outside edge of the last node in the
   *   container.
   */
  find(aContainer, aX, aY) {
    let closest = null;
    let minCartesian = Number.MAX_VALUE;
    let containerX = this.#containerInfo.left;
    let containerY = this.#containerInfo.top;

    // First, iterate through all children and find the closest child to the
    // aX and aY coordinates (preferring children in the same row as the aX
    // and aY coordinates).
    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 refine our result based on whether or not we're closer to the outside
    // edge of the closest node. If we are, we actually want to return the
    // closest node's sibling, because this is the one we'll style to indicate
    // the drop position.
    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.
   *
   * @param {DOMNode} aContainer
   *   The container of the nodes for which we are inserting the placeholder
   *   and shifting the child nodes.
   * @param {DOMNode} aBefore
   *   The child node before which we are inserting the placeholder.
   * @param {DOMRectReadOnly} aSize
   *   The size of the placeholder to create.
   * @param {boolean} aIsFromThisArea
   *   True if the node being dragged happens to be from this container, as
   *   opposed to some other container (like a toolbar, for instance).
   */
  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 and apply it.
        child.style.transform = this.#diffWithNext(child, aSize);
      } else {
        // If we're not shifting this node, reset the transform
        child.style.transform = "";
      }
    }

    // Bug 959848: without this routine, when we start the drag of an item in
    // the customization palette, we'd take the dragged item out of the flow of
    // the document, and _then_ insert the placeholder, creating a lot of motion
    // on the initial drag. We mask this case by removing the item and inserting
    // the placeholder for the dragged item in a single shot without animation.
    if (
      aContainer.lastElementChild &&
      aIsFromThisArea &&
      !this.#lastPlaceholderInsertion
    ) {
      // Flush layout to force the snap transition.
      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 {DOMNode} aContainer
   *   The container in which to reset the transforms.
   * @param {boolean} aNoTransition
   *   If truthy, adds a notransition attribute to the node while resetting the
   *   transform. It is assumed that a CSS rule will interpret the notransition
   *   attribute as a directive to skip transition animations.
   */
  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;
    }
  }

  /**
   * Determines the transform rule to apply to aNode to reposition it to
   * accommodate a placeholder drop target for a dragged node of aSize.
   *
   * @param {DOMNode} aNode
   *   The node to calculate the transform rule for.
   * @param {DOMRectReadOnly} aSize
   *   The size of the placeholder drop target that was inserted which then
   *   requires us to reposition this node.
   * @returns {string}
   *   The CSS transform rule to apply to aNode.
   */
  #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 horizontal transform value for a node if there
   * isn't a next node to base that on.
   *
   * @param {DOMNode} aNode
   *   The node to have the transform applied to.
   * @param {DOMRectReadOnly} aNodeBounds
   *   The bounding rect info of aNode.
   * @param {DOMNode} aFirstNodeInRow
   *   The first node in aNode's row in the container grid.
   * @returns {number}
   *   The horizontal distance to transform aNode.
   */
  #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 the DOMRectReadOnly for a node from our cache. If the rect is not yet
   * cached, calculate that rect and cache it now.
   *
   * @param {DOMNode} aNode
   *   The node whose DOMRectReadOnly that we want.
   * @returns {DOMRectReadOnly}
   *   The size and position of aNode that was either just calculated, or
   *   previously calculated during the lifetime of this AreaPositionManager and
   *   cached.
   */
  #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 as a
      // DOMRectReadOnly.
      rect = DOMRectReadOnly.fromRect(aNode.getBoundingClientRect());
      this.#nodePositionStore.set(aNode, rect);
    }
    return rect;
  }

  /**
   * Returns the first node in aNode's row in the container grid.
   *
   * @param {DOMNode} aNode
   *   The node in the row for which we want to find the first node.
   * @returns {DOMNode}
   */
  #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;
  }

  /**
   * Returns the next visible sibling DOMNode to aNode in the direction
   * aDirection.
   *
   * @param {DOMNode} aNode
   *   The node to get the next visible sibling for.
   * @param {string} aDirection
   *   One of either "previous" or "next". Any other value will probably throw.
   * @returns {DOMNode}
   */
  #getVisibleSiblingForDirection(aNode, aDirection) {
    let rv = aNode;
    do {
      rv = rv[aDirection + "ElementSibling"];
    } while (rv && rv.hidden);
    return rv;
  }
}

/**
 * DragPositionManager manages the AreaPositionManagers for all of the
 * grid-like customizable areas. These days, that's just the customization
 * palette.
 */
export var DragPositionManager = {
  /**
   * Starts CustomizeMode drag position management for a window aWindow.
   *
   * @param {DOMWindow} aWindow
   *   The browser window to start drag position management in.
   */
  start(aWindow) {
    let paletteArea = aWindow.document.getElementById(kPaletteId);
    let positionManager = gManagers.get(paletteArea);
    if (positionManager) {
      positionManager.update(paletteArea);
    } else {
      // This gManagers WeakMap may have made more sense when we had the
      // menu panel also acting as a grid. It's maybe superfluous at this point.
      gManagers.set(paletteArea, new AreaPositionManager(paletteArea));
    }
  },

  /**
   * Stops CustomizeMode drag position management for all windows.
   */
  stop() {
    gManagers = new WeakMap();
  },

  /**
   * Returns the AreaPositionManager instance for a particular aArea DOMNode,
   * if one has been created.
   *
   * @param {DOMNode} aArea
   * @returns {AreaPositionManager|null}
   */
  getManagerForArea(aArea) {
    return gManagers.get(aArea);
  },
};

Object.freeze(DragPositionManager);