bm-core-ui 2.11.8 → 2.11.9-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,172 @@
1
1
  // @ts-check
2
2
 
3
- import { BMExtend, NO, YES, BMCopyProperties, BMNumberByConstrainingNumberToBounds, BMUUIDMake } from "../../Core/BMCoreUI";
3
+ import { BMExtend, NO, YES, BMCopyProperties, BMNumberByConstrainingNumberToBounds, BMUUIDMake, BMNumberByInterpolatingNumbersWithFraction } from "../../Core/BMCoreUI";
4
4
  import { BMWindow } from "../BMWindow";
5
5
  import { BMRectMakeWithOrigin, BMRectMakeWithNodeFrame, BMRectMake } from "../../Core/BMRect";
6
6
  import { BMPointMake } from "../../Core/BMPoint";
7
- import { BMHook, __BMVelocityAnimate } from "../../Core/BMAnimationContext";
7
+ import { BMAnimateWithBlock, BMAnimationApplyBlocking, BMAnimationBeginWithDuration, BMAnimationContext, BMAnimationContextAddCompletionHandler, BMAnimationContextBeginStatic, BMAnimationContextGetCurrent, BMHook, __BMVelocityAnimate } from "../../Core/BMAnimationContext";
8
8
  import { BMView, BMViewColorScheme } from "../../BMView/BMView_v2.5";
9
9
  import { BMInsetMakeWithEqualInsets } from "../../Core/BMInset";
10
10
 
11
- // @type BMPopoverIndicatorDirection
11
+
12
+ /**
13
+ * The number of pixels a touch or clicked pointer can wander off before causing a popover to detach.
14
+ */
15
+ const BMPopoverDragThreshold = 128; // <Number>
16
+
17
+ // @type _BMPopoverDisplayConfiguration implements BMAnimating
18
+
19
+ /**
20
+ * A class that describes the display attributes of a popover and supports interpolation.
21
+ */
22
+ function _BMPopoverDisplayConfiguration() {} // <constructor>
23
+
24
+ _BMPopoverDisplayConfiguration.prototype = {
25
+ /**
26
+ * The popover's frame, excluding the indicator's size.
27
+ */
28
+ _frame: undefined, // <BMRect>
29
+
30
+ /**
31
+ * The indicator's direction relative to the popover's anchor.
32
+ */
33
+ _direction: undefined, // <BMPopoverIndicatorDirection>
34
+
35
+ /**
36
+ * The indicator's position along the popover's edge.
37
+ */
38
+ _indicatorPosition: undefined, // <Number>
39
+
40
+ /**
41
+ * The size of the indicator.
42
+ */
43
+ _indicatorSize: undefined, // <Number>
44
+
45
+ /**
46
+ * The round radius of the popover's corners.
47
+ */
48
+ _borderRadius: undefined, // <Number>
49
+
50
+ /**
51
+ * The height of the visible portion of the indicator.
52
+ */
53
+ get _indicatorHeight() { // <Number>
54
+ // The indicator height represents the edge size of the indicator square;
55
+ // Therefore its total width/height represents the diagonal of that square
56
+ // and its visible height is half of the square's diagonal
57
+ const indicatorWidth = this._indicatorSize * Math.SQRT2;
58
+ return indicatorWidth / 2 | 0;
59
+ },
60
+
61
+ /**
62
+ * Initializes this popover popover display configuration object with the specified values.
63
+ * @param frame <BMRect> The popover's frame.
64
+ * {
65
+ * @param direction <BMPopoverIndicatorDirection> The indicator's direction relative to the popover's anchor.
66
+ * @param indicatorPosition <Number> The indicator's position relative to the popover's edge.
67
+ * @param indicatorSize <Number> The size of the indicator.
68
+ * @param borderRadius <Number> The size of the popover's corners.
69
+ * }
70
+ * @returns <_BMPopoverDisplayConfiguration> This popover display configuration.
71
+ */
72
+ initWithFrame(frame, {direction, indicatorPosition, indicatorSize, borderRadius}) {
73
+ this._frame = frame.copy();
74
+ this._direction = direction;
75
+ this._indicatorPosition = indicatorPosition;
76
+ this._indicatorSize = indicatorSize;
77
+ this._borderRadius = borderRadius;
78
+
79
+ return this;
80
+ },
81
+
82
+ /**
83
+ * Initializes this popover popover display configuration object by copying the values of
84
+ * the specified popover display configuration.
85
+ * @param config <_BMPopoverDisplayConfiguration> The configuration whose values to copy.
86
+ * @returns <_BMPopoverDisplayConfiguration> This popover display configuration.
87
+ */
88
+ initWithPopoverDisplayConfiguration(config) {
89
+ this._frame = config._frame.copy();
90
+ this._direction = config._direction;
91
+ this._indicatorPosition = config._indicatorPosition;
92
+ this._indicatorSize = config._indicatorSize;
93
+ this._borderRadius = config._borderRadius;
94
+
95
+ return this;
96
+ },
97
+
98
+ /**
99
+ * Creates and returns a copy of this popover display configuration.
100
+ * @returns <_BMPopoverDisplayConfiguration> A copy of this object.
101
+ */
102
+ copy() {
103
+ return (new _BMPopoverDisplayConfiguration).initWithPopoverDisplayConfiguration(this);
104
+ },
105
+
106
+ /**
107
+ * Invoked by the CoreUI animation engine to obtain an interpolated
108
+ * value between this object and the target object.
109
+ * @param fraction <Number> The animation fraction.
110
+ * {
111
+ * @param toValue <_BMPopoverDisplayConfiguration> The object to which to interpolate.
112
+ * }
113
+ * @return <_BMPopoverDisplayConfiguration> A popover display configuration.
114
+ */
115
+ interpolatedValueWithFraction(fraction, {toValue: target}) {
116
+ const copy = this.copy();
117
+
118
+ copy._frame = this._frame.interpolatedValueWithFraction(fraction, {toValue: target._frame});
119
+
120
+ if (this._direction == target._direction) {
121
+ // When the direction remains the same animate all other indicator properties smoothly
122
+ copy._indicatorPosition = BMNumberByInterpolatingNumbersWithFraction(this._indicatorPosition, target._indicatorPosition, fraction);
123
+ copy._indicatorSize = BMNumberByInterpolatingNumbersWithFraction(this._indicatorSize, target._indicatorSize, fraction);
124
+ }
125
+ else {
126
+ // Otherwise hide then indicator from its source direction then reveal it at the target direction over the course of the animation
127
+ if (fraction < 0.5) {
128
+ // In the first half, hide the indicator, keeping it in the source direction
129
+ copy._direction = this._direction;
130
+ copy._indicatorPosition = this._indicatorPosition;
131
+ copy._indicatorSize = BMNumberByInterpolatingNumbersWithFraction(this._indicatorSize, 0, fraction * 2);
132
+ }
133
+ else {
134
+ // In the second half, show the indicator, keeping it in the target direction
135
+ copy._direction = target._direction;
136
+ copy._indicatorPosition = target._indicatorPosition;
137
+ copy._indicatorSize = BMNumberByInterpolatingNumbersWithFraction(0, target._indicatorSize, (fraction - 0.5) * 2);
138
+ }
139
+ }
140
+
141
+ copy._borderRadius = BMNumberByInterpolatingNumbersWithFraction(this._borderRadius, target._borderRadius, fraction);
142
+
143
+ return copy;
144
+ },
145
+ };
146
+
147
+ /**
148
+ * Creates and returns a popover display configuration object initialized with the specified values.
149
+ * @param frame <BMRect> The popover's frame.
150
+ * {
151
+ * @param direction <BMPopoverIndicatorDirection> The indicator's direction relative to the popover's anchor.
152
+ * @param indicatorPosition <Number> The indicator's position relative to the popover's edge.
153
+ * @param indicatorSize <Number> The size of the indicator.
154
+ * @param borderRadius <Number> The size of the popover's corners.
155
+ * }
156
+ * @returns <_BMPopoverDisplayConfiguration> A popover display configuration.
157
+ */
158
+ _BMPopoverDisplayConfiguration.configurationWithFrame = function (frame, args) {
159
+ return (new this).initWithFrame(frame, args);
160
+ };
12
161
 
13
162
  // @endtype
14
163
 
164
+ // @type BMPopoverIndicatorDirection
165
+
15
166
  /**
16
167
  * Constants describing the position where the indicator appears on its popover.
17
168
  */
18
- export var BMPopoverIndicatorDirection = Object.freeze({ // <enum>
169
+ export var BMPopoverIndicatorDirection = Object.freeze({ // <enum>
19
170
  /**
20
171
  * Causes the popover indicator to appear on the top edge of the popover.
21
172
  */
@@ -37,6 +188,8 @@ import { BMInsetMakeWithEqualInsets } from "../../Core/BMInset";
37
188
  Right: "Right" // <enum>
38
189
  });
39
190
 
191
+ // @endtype
192
+
40
193
  // @type BMPopover extends BMWindow
41
194
 
42
195
  /**
@@ -49,9 +202,14 @@ import { BMInsetMakeWithEqualInsets } from "../../Core/BMInset";
49
202
  export function BMPopover() {} // <constructor>
50
203
 
51
204
  BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
205
+
206
+ /**
207
+ * An optional delegate which this popover may notify of key events.
208
+ */
209
+ delegate: undefined, // <BMPopoverDelegate, nullable>
52
210
 
53
211
  /**
54
- * The point from which this popover should originate, relative to the document. Either this property
212
+ * Animatable. The point from which this popover should originate, relative to the document. Either this property
55
213
  * or `anchorNode` or `anchorRect` must be set before this popover is displayed.
56
214
  */
57
215
  _anchorPoint: undefined, // <BMPoint, nullable>
@@ -60,12 +218,16 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
60
218
  return this._anchorPoint;
61
219
  },
62
220
  set anchorPoint(point) {
221
+ this._anchorRect = undefined;
222
+ this._anchorNode = undefined;
63
223
  this._anchorPoint = point;
224
+
225
+ this._updatePosition();
64
226
  },
65
227
 
66
228
 
67
229
  /**
68
- * The rect from which this popover should originate, relative to the document. Either this property
230
+ * Animatable. The rect from which this popover should originate, relative to the document. Either this property
69
231
  * or `anchorPoint` or `anchorNode` must be set before this popover is displayed.
70
232
  */
71
233
  _anchorRect: undefined, // <BMRect, nullable>
@@ -74,12 +236,16 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
74
236
  return this._anchorRect;
75
237
  },
76
238
  set anchorRect(rect) {
239
+ this._anchorPoint = undefined;
240
+ this._anchorNode = undefined;
77
241
  this._anchorRect = rect;
242
+
243
+ this._updatePosition();
78
244
  },
79
245
 
80
246
 
81
247
  /**
82
- * The element from which this popover should originate. Either this property
248
+ * Animatable. The element from which this popover should originate. Either this property
83
249
  * or `anchorPoint` or `anchorRect` must be set before this popover is displayed.
84
250
  */
85
251
  _anchorNode: undefined, // <DOMNode, nullable>
@@ -88,7 +254,11 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
88
254
  return this._anchorNode;
89
255
  },
90
256
  set anchorNode(node) {
257
+ this._anchorRect = undefined;
258
+ this._anchorPoint = undefined;
91
259
  this._anchorNode = node;
260
+
261
+ this._updatePosition();
92
262
  },
93
263
 
94
264
  /**
@@ -101,6 +271,8 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
101
271
  },
102
272
  set size(size) {
103
273
  this._size = size.copy();
274
+
275
+ this._updatePosition();
104
276
  },
105
277
 
106
278
  /**
@@ -113,6 +285,8 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
113
285
  },
114
286
  set indicatorSize(size) {
115
287
  this._indicatorSize = size;
288
+
289
+ this._updatePosition();
116
290
  },
117
291
 
118
292
  /**
@@ -125,6 +299,8 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
125
299
  },
126
300
  set borderRadius(radius) {
127
301
  this._borderRadius = radius;
302
+
303
+ this._updatePosition();
128
304
  },
129
305
 
130
306
  /**
@@ -139,11 +315,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
139
315
  set edgeInsets(margin) {
140
316
  this._edgeInsets = margin || BMInsetMakeWithEqualInsets(8);
141
317
 
142
- if (this.isVisible) {
143
- // If this is updated while the popover is visible, update its position accordingly
144
- // TODO: Only update when this would actually change the position
145
- this._updatePosition();
146
- }
318
+ this._updatePosition();
147
319
  },
148
320
 
149
321
  /**
@@ -244,6 +416,8 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
244
416
  },
245
417
  set permittedDirections(directions) {
246
418
  this._permittedDirections = directions.slice();
419
+
420
+ this._updatePosition();
247
421
  },
248
422
 
249
423
  /**
@@ -340,119 +514,237 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
340
514
  },
341
515
 
342
516
  /**
343
- * Invoked by CoreUI to update this popover's position and recalculate the various paths used by it.
517
+ * Set to `YES` while this popover is detached. When set to `YES` the popover is detached from
518
+ * its anchor and can be freely moved. When set to `NO`, the popover remains attached to its
519
+ * anchor in a fixed position.
344
520
  */
345
- _updatePosition() {
521
+ _isDetached: NO, // <Boolean>
522
+
523
+ get isDetached() {
524
+ return this._isDetached;
525
+ },
526
+
527
+ /**
528
+ * The current detached position of this popover.
529
+ */
530
+ _position: undefined, // <BMPoint>
531
+
532
+ /**
533
+ * The popover's current direction. Set to `undefined` until this popover is first displayed.
534
+ */
535
+ _direction: undefined, // <BMPopoverIndicatorDirection, nullable>
536
+
537
+ /**
538
+ * Controls whether this popover retains the direction that was set to it when it was first
539
+ * displayed. When set to `NO`, whenever the popover's size or attributes change it will try
540
+ * to find a new direction around the anchor. When set to `YES`, the current direction or the
541
+ * initial display direction will be kept regardless of how the popover changes.
542
+ *
543
+ * When set to `undefined`, this popover will use the behaviour set on the `BMPopover` class.
544
+ */
545
+ _retainsDirection: undefined, // <Boolean, nullable>
546
+
547
+ get retainsDirection() {
548
+ return this._retainsDirection ?? BMPopover._retainsDirection;
549
+ },
550
+
551
+ set retainsDirection(retains) {
552
+ this._retainsDirection = retains;
553
+ },
554
+
555
+ /**
556
+ * The current display configuration. Set to `undefined` until this popover is first displayed.
557
+ */
558
+ _config: undefined, // <_BMPopoverDisplayConfiguration, nullable>
559
+
560
+ /**
561
+ * The current display configuration. Set to `undefined` until this popover is first displayed.
562
+ */
563
+ get _displayConfiguration() { // <_BMPopoverDisplayConfiguration, nullable>
564
+ return this._config;
565
+ },
566
+
567
+ set _displayConfiguration(config) {
568
+ this._config = config;
569
+ this._applyDisplayConfiguration(config);
570
+ },
571
+
572
+ /**
573
+ * Invoked by CoreUI to create a new display configuration for this popover based on the
574
+ * current values of its properties.
575
+ * @return <_BMPopoverDisplayConfiguration> The configuration.
576
+ */
577
+ _createDisplayConfiguration() {
346
578
  const frame = BMRectMake();
347
- frame.size.height = this._size.height + this._indicatorHeight;
579
+ frame.size.height = this._size.height;
348
580
  frame.size.width = this._size.width;
581
+ frame.origin.x = (this._position?.x ?? this._frame.origin.x) | 0;
582
+ frame.origin.y = (this._position?.y ?? this._frame.origin.y) | 0;
349
583
 
350
584
  const nodeFrame = this.anchorRect || (this.anchorNode && BMRectMakeWithNodeFrame(this.anchorNode));
351
585
  const location = this.anchorPoint ? this.anchorPoint.copy() : nodeFrame.center;
352
586
 
353
587
  // Determine the appropriate direction to display this popover
354
588
  let direction;
355
- if (this.anchorPoint) {
589
+ if (this._direction && this.retainsDirection) {
590
+ direction = this._direction;
591
+ }
592
+ else if (this.anchorPoint) {
356
593
  direction = this._directionAroundPoint(location);
357
594
  }
358
595
  else {
359
596
  direction = this._directionAroundRect(nodeFrame);
360
597
  }
361
598
 
362
- // Adjust the constraints based on the direction
599
+ if (!this._isDetached) {
600
+ // If this popover is attached, update the frame's position based on the anchor
601
+ // Move the frame to the appropriate position based on the selected direction
602
+ switch (direction) {
603
+ case BMPopoverIndicatorDirection.Top:
604
+ frame.size.height += this._indicatorHeight;
605
+ frame.origin.y = this.anchorPoint ? location.y : nodeFrame.bottom - 2;
606
+
607
+ frame.origin.x = location.x - frame.size.width / 2 | 0;
608
+
609
+ if (frame.origin.x < this._edgeInsets.left) {
610
+ frame.origin.x = this._edgeInsets.left;
611
+ }
612
+ if (frame.right > window.innerWidth - this._edgeInsets.right) {
613
+ frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
614
+ }
615
+ break;
616
+ case BMPopoverIndicatorDirection.Bottom:
617
+ frame.size.height += this._indicatorHeight;
618
+ frame.origin.y = this.anchorPoint ? location.y - frame.size.height : nodeFrame.origin.y + 2 - frame.size.height;
619
+
620
+ frame.origin.x = location.x - frame.size.width / 2 | 0;
621
+
622
+ if (frame.origin.x < this._edgeInsets.left) {
623
+ frame.origin.x = this._edgeInsets.left;
624
+ }
625
+ if (frame.right > window.innerWidth - this._edgeInsets.right) {
626
+ frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
627
+ }
628
+ break;
629
+ case BMPopoverIndicatorDirection.Right:
630
+ frame.size.width += this._indicatorHeight;
631
+ frame.origin.x = this.anchorPoint ? location.x - frame.size.width : nodeFrame.origin.x + 2 - frame.size.width;
632
+
633
+ frame.origin.y = location.y - frame.size.height / 2 | 0;
634
+
635
+ if (frame.origin.y < this._edgeInsets.top) {
636
+ frame.origin.y = this._edgeInsets.top;
637
+ }
638
+ if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
639
+ frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
640
+ }
641
+ break;
642
+ case BMPopoverIndicatorDirection.Left:
643
+ frame.size.width += this._indicatorHeight;
644
+ frame.origin.x = this.anchorPoint ? location.x : nodeFrame.right - 2;
645
+
646
+ frame.origin.y = location.y - frame.size.height / 2 | 0;
647
+
648
+ if (frame.origin.y < this._edgeInsets.top) {
649
+ frame.origin.y = this._edgeInsets.top;
650
+ }
651
+ if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
652
+ frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
653
+ }
654
+ break;
655
+ }
656
+ }
657
+ else {
658
+ // If the popover is detached, prevent it from exceeding the screen's size
659
+ frame.size.width = BMNumberByConstrainingNumberToBounds(frame.size.width, 0, window.innerWidth);
660
+ frame.size.height = BMNumberByConstrainingNumberToBounds(frame.size.height, 0, window.innerHeight);
661
+ frame.origin.x = BMNumberByConstrainingNumberToBounds(frame.origin.x, 0, window.innerWidth - frame.size.width);
662
+ frame.origin.y = BMNumberByConstrainingNumberToBounds(frame.origin.y, 0, window.innerHeight - frame.size.height);
663
+ }
664
+
665
+ // Determine the indicator's position along its edge
666
+ let indicatorPosition;
363
667
  switch (direction) {
668
+ case BMPopoverIndicatorDirection.Bottom:
364
669
  case BMPopoverIndicatorDirection.Top:
365
- frame.origin.y = this.anchorPoint ? location.y : nodeFrame.bottom - 2;
366
-
367
- frame.origin.x = location.x - frame.size.width / 2 | 0;
368
-
369
- if (frame.origin.x < this._edgeInsets.left) {
370
- frame.origin.x = this._edgeInsets.left;
670
+ if (!this._isDetached) {
671
+ frame.size.height -= this._indicatorHeight;
371
672
  }
372
- if (frame.right > window.innerWidth - this._edgeInsets.right) {
373
- frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
673
+ indicatorPosition = BMNumberByConstrainingNumberToBounds(location.x - frame.origin.x, this._borderRadius * 1.5 + this._indicatorSize * Math.SQRT2 / 2, frame.size.width - this._borderRadius * 1.5 - this._indicatorSize * Math.SQRT2 / 2);
674
+ break;
675
+ case BMPopoverIndicatorDirection.Left:
676
+ case BMPopoverIndicatorDirection.Right:
677
+ if (!this._isDetached) {
678
+ frame.size.width -= this._indicatorHeight;
374
679
  }
375
-
376
- this._contentViewTopConstraint.constant = this._indicatorHeight;
680
+ indicatorPosition = BMNumberByConstrainingNumberToBounds(location.y - frame.origin.y, this._borderRadius * 1.5 + this._indicatorSize * Math.SQRT2 / 2, frame.size.height - this._borderRadius * 1.5 - this._indicatorSize * Math.SQRT2 / 2);
681
+ break;
682
+ }
683
+
684
+ const indicatorSize = this._isDetached ? 0 : this._indicatorSize;
685
+
686
+ const configuration = _BMPopoverDisplayConfiguration.configurationWithFrame(frame, {direction, indicatorSize, indicatorPosition, borderRadius: this._borderRadius});
687
+ return configuration;
688
+ },
689
+
690
+ /**
691
+ * Invoked by CoreUI to apply the specified display configuration to this popover and its various SVG elements.
692
+ * @param config <_BMPopoverDisplayConfiguration> The configuration to apply.
693
+ */
694
+ _applyDisplayConfiguration(config) {
695
+ if (this.__released) {
696
+ // Prevent configuration applications from throwing an error if a popover was released while an
697
+ // animation was playing
698
+ return;
699
+ }
700
+
701
+ const frame = config._frame.copy();
702
+ const indicatorHeight = config._indicatorHeight;
703
+ const direction = config._direction;
704
+
705
+ // Adjust the constraints based on the direction
706
+ switch (direction) {
707
+ case BMPopoverIndicatorDirection.Top:
708
+ frame.size.height += indicatorHeight;
709
+ this._contentViewTopConstraint.constant = indicatorHeight;
377
710
  this._contentViewBottomConstraint.constant = 0;
378
711
  this._contentViewLeftConstraint.constant = 0;
379
712
  this._contentViewRightConstraint.constant = 0;
380
713
  break;
381
714
  case BMPopoverIndicatorDirection.Bottom:
382
- frame.origin.y = this.anchorPoint ? location.y - frame.size.height : nodeFrame.origin.y + 2 - frame.size.height;
383
-
384
- frame.origin.x = location.x - frame.size.width / 2 | 0;
385
-
386
- if (frame.origin.x < this._edgeInsets.left) {
387
- frame.origin.x = this._edgeInsets.left;
388
- }
389
- if (frame.right > window.innerWidth - this._edgeInsets.right) {
390
- frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
391
- }
392
-
715
+ frame.size.height += indicatorHeight;
393
716
  this._contentViewTopConstraint.constant = 0;
394
- this._contentViewBottomConstraint.constant = -this._indicatorHeight;
717
+ this._contentViewBottomConstraint.constant = -indicatorHeight;
395
718
  this._contentViewLeftConstraint.constant = 0;
396
719
  this._contentViewRightConstraint.constant = 0;
397
720
  break;
398
721
  case BMPopoverIndicatorDirection.Right:
399
- frame.origin.x = this.anchorPoint ? location.x - frame.size.width : nodeFrame.origin.x + 2 - frame.size.width;
400
-
401
- frame.origin.y = location.y - frame.size.height / 2 | 0;
402
-
403
- if (frame.origin.y < this._edgeInsets.top) {
404
- frame.origin.y = this._edgeInsets.top;
405
- }
406
- if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
407
- frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
408
- }
409
-
722
+ frame.size.width += indicatorHeight;
410
723
  this._contentViewTopConstraint.constant = 0;
411
724
  this._contentViewBottomConstraint.constant = 0;
412
725
  this._contentViewLeftConstraint.constant = 0;
413
- this._contentViewRightConstraint.constant = -this._indicatorHeight;
726
+ this._contentViewRightConstraint.constant = -indicatorHeight;
414
727
  break;
415
728
  case BMPopoverIndicatorDirection.Left:
416
- frame.origin.x = this.anchorPoint ? location.x : nodeFrame.right - 2;
417
-
418
- frame.origin.y = location.y - frame.size.height / 2 | 0;
419
-
420
- if (frame.origin.y < this._edgeInsets.top) {
421
- frame.origin.y = this._edgeInsets.top;
422
- }
423
- if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
424
- frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
425
- }
426
-
729
+ frame.size.width += indicatorHeight;
427
730
  this._contentViewTopConstraint.constant = 0;
428
731
  this._contentViewBottomConstraint.constant = 0;
429
- this._contentViewLeftConstraint.constant = this._indicatorHeight;
732
+ this._contentViewLeftConstraint.constant = indicatorHeight;
430
733
  this._contentViewRightConstraint.constant = 0;
431
734
  break;
432
735
  }
433
736
 
434
- this.frame = frame;
435
-
436
- // Determine the indicator's position along its edge
437
- let indicatorPosition;
438
- switch (direction) {
439
- case BMPopoverIndicatorDirection.Bottom:
440
- case BMPopoverIndicatorDirection.Top:
441
- indicatorPosition = BMNumberByConstrainingNumberToBounds(location.x - frame.origin.x, 12, frame.size.width - 12);
442
- break;
443
- case BMPopoverIndicatorDirection.Left:
444
- case BMPopoverIndicatorDirection.Right:
445
- indicatorPosition = BMNumberByConstrainingNumberToBounds(location.y - frame.origin.y, 12, frame.size.height - 12);
446
- break;
447
- }
737
+ const indicatorPosition = config._indicatorPosition;
738
+ const borderRadius = config._borderRadius;
739
+ const indicatorSize = config._indicatorSize;
448
740
 
449
741
  // Create an inner frame to be used by the various paths
450
742
  const innerFrame = frame.copy();
451
743
  innerFrame.origin = BMPointMake();
452
744
 
453
- const pathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize: this._indicatorSize, position: indicatorPosition, direction, radius: this._borderRadius})}`;
454
- const outlinePathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize: this._indicatorSize, inset: 1, position: indicatorPosition, direction, radius: this._borderRadius - 1.5})}`;
455
- const boxShadowPathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize: this._indicatorSize, position: indicatorPosition, direction, radius: this._borderRadius + 1.5})}`;
745
+ const pathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize, position: indicatorPosition, direction, radius: borderRadius})}`;
746
+ const outlinePathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize, inset: 1, position: indicatorPosition, direction, radius: borderRadius - 1.5})}`;
747
+ const boxShadowPathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize, position: indicatorPosition, direction, radius: borderRadius + 1.5})}`;
456
748
 
457
749
  if (!this._clipPathUUID && !CSS.supports('clip-path', `path('${pathContent}')`)) {
458
750
  // If inline path definitions are not supported by the browsers, create an UUID for a SVG clip path and create it
@@ -514,11 +806,24 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
514
806
  };
515
807
  BMCopyProperties(this.contentNode.style, positionStyle);
516
808
 
809
+ const popoverDarkModeFill = this._background.querySelector('.BMPopoverBackgroundDarkModeFill');
810
+
811
+ // Safari requires a forced style recalculation for the drop shadow and dark mode fill during animations
812
+ if (BMPopover._requiresClipPathReflow) {
813
+ popoverDarkModeFill.style.clipPath = 'none';
814
+ popoverDarkModeFill.style.webkitClipPath = 'none';
815
+ this._dropShadowContent.style.clipPath = 'none';
816
+ this._dropShadowContent.style.webkitClipPath = 'none';
817
+
818
+ // These trigger a forced reflow
819
+ popoverDarkModeFill.offsetWidth;
820
+ this._dropShadowContainer.offsetWidth;
821
+ }
822
+
517
823
  BMCopyProperties(this._background.style, positionStyle);
518
824
  this._background.style.clipPath = path;
519
825
  this._background.style.webkitClipPath = path;
520
826
 
521
- const popoverDarkModeFill = this._background.querySelector('.BMPopoverBackgroundDarkModeFill');
522
827
  popoverDarkModeFill.style.clipPath = outlinePath;
523
828
  popoverDarkModeFill.style.webkitClipPath = outlinePath;
524
829
 
@@ -531,26 +836,85 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
531
836
  let transformOriginX, transformOriginY;
532
837
  switch (direction) {
533
838
  case BMPopoverIndicatorDirection.Bottom:
534
- transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
839
+ transformOriginX = ((indicatorPosition / frame.size.width) * 100) + '%';
535
840
  transformOriginY = '100%';
536
841
  break;
537
842
  case BMPopoverIndicatorDirection.Top:
538
- transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
843
+ transformOriginX = ((indicatorPosition / frame.size.width) * 100) + '%';
539
844
  transformOriginY = '0%';
540
845
  break;
541
846
  case BMPopoverIndicatorDirection.Left:
542
847
  transformOriginX = '0%';
543
- transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
848
+ transformOriginY = ((indicatorPosition / frame.size.height) * 100) + '%';
544
849
  break;
545
850
  case BMPopoverIndicatorDirection.Right:
546
851
  transformOriginX = '100%';
547
- transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
852
+ transformOriginY = ((indicatorPosition / frame.size.height) * 100) + '%';
548
853
  break;
549
854
  }
550
855
 
551
856
  for (const layer of popoverLayers) {
552
857
  layer.style.transformOrigin = `${transformOriginX} ${transformOriginY}`;
553
858
  }
859
+
860
+ this.frame = frame;
861
+ this.layoutIfNeeded();
862
+ },
863
+
864
+ /**
865
+ * The number of display configuration animations running currently against this popover's display configuration.
866
+ */
867
+ _displayConfigurationAnimations: 0, // <Number>
868
+
869
+ /**
870
+ * A display configuration that will be applied to this popover at the end of any current
871
+ * animations affecting the display configuration.
872
+ */
873
+ _pendingDisplayConfiguration: undefined, // <_BMPopoverDisplayConfiguration>
874
+
875
+ /**
876
+ * Animatable. Invoked by CoreUI to update this popover's position and recalculate the various paths used by it.
877
+ */
878
+ _updatePosition() {
879
+ // If the popover is not currently visible, there is not action to take
880
+ if (!this.isVisible) {
881
+ return;
882
+ }
883
+
884
+ const config = this._createDisplayConfiguration();
885
+
886
+ const context = BMAnimationContextGetCurrent();
887
+ if (context) {
888
+ const controller = context.controllerForObject(this, {node: this.node});
889
+ controller.registerAnimatableProperty('_displayConfiguration', {targetValue: config});
890
+
891
+ this._displayConfigurationAnimations++;
892
+ // When all animations finish on this popover, if there was any pending display configuration, apply it then
893
+ BMAnimationContextAddCompletionHandler(() => {
894
+ this._displayConfigurationAnimations--;
895
+
896
+ if (!this.isVisible) {
897
+ return;
898
+ }
899
+
900
+ if (!this._displayConfigurationAnimations && this._pendingDisplayConfiguration) {
901
+ BMAnimationContextBeginStatic(); {
902
+ this._displayConfiguration = this._pendingDisplayConfiguration;
903
+ this._pendingDisplayConfiguration = undefined;
904
+ } BMAnimationApplyBlocking();
905
+ }
906
+ });
907
+ }
908
+ else {
909
+ if (this._displayConfigurationAnimations) {
910
+ // If an animation is in progress, wait for it to finish before applying the new configuration
911
+ this._pendingDisplayConfiguration = config;
912
+ }
913
+ else {
914
+ // Else apply the configuration directly
915
+ this._displayConfiguration = config;
916
+ }
917
+ }
554
918
  },
555
919
 
556
920
  /**
@@ -723,6 +1087,138 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
723
1087
  return path;
724
1088
  },
725
1089
 
1090
+ /**
1091
+ * Controls whether an in-progress drag can detach this popover.
1092
+ */
1093
+ _canDetach: NO, // <Boolean>
1094
+
1095
+ /**
1096
+ * @protected
1097
+ * Controls whether this popover is detachable. Defaults to the result provided
1098
+ * by the delegate.
1099
+ * @return <Boolean> `YES` if the popover is detachable, `NO` otherwise.
1100
+ */
1101
+ isDetachable() {
1102
+ return this.delegate?.popoverCanDetach?.(this) ?? NO;
1103
+ },
1104
+
1105
+ /**
1106
+ * During a drag operation set to the point where the drag began.
1107
+ */
1108
+ _initialDragPosition: undefined, // <BMPoint, nullable>
1109
+
1110
+ // @override - BMWindow
1111
+ dragBeganAtPosition(position, {withEvent: event}) {
1112
+ this._canDetach = this.isDetachable();
1113
+
1114
+ // If the popover is neither detached nor can it detach, drags cannot be performed
1115
+ if (!this._canDetach && !this._isDetached) {
1116
+ return;
1117
+ }
1118
+
1119
+ this._initialDragPosition = position.copy();
1120
+ BMWindow.prototype.dragBeganAtPosition.apply(this, arguments);
1121
+ },
1122
+
1123
+ // @override - BMWindow
1124
+ dragPositionDidChangeFromPosition(fromPosition, {toPosition, event}) {
1125
+ if (!this._canDetach && !this._isDetached) {
1126
+ return;
1127
+ }
1128
+
1129
+ if (!this._isDetached) {
1130
+ // If not detached, apply a displacement to this popover that is half of the regular
1131
+ // drag distance, until this popover is fully detached
1132
+ BMHook(this.node, {
1133
+ translateX: `${(toPosition.x - this._initialDragPosition.x) / 3 | 0}px`,
1134
+ translateY: `${(toPosition.y - this._initialDragPosition.y) / 3 | 0}px`,
1135
+ });
1136
+
1137
+ const distance = toPosition.distanceToPoint(this._initialDragPosition);
1138
+ if (distance > BMPopoverDragThreshold) {
1139
+ // If the movement exceeds the detachment threshold, detach this popover
1140
+ this.detachAnimated(YES);
1141
+ }
1142
+ }
1143
+ else {
1144
+ // If this is detached, update the popover's position
1145
+ this._position = BMPointMake(
1146
+ BMNumberByConstrainingNumberToBounds(this._position.x + toPosition.x - fromPosition.x, 0, window.innerWidth - this.frame.size.width),
1147
+ BMNumberByConstrainingNumberToBounds(this._position.y + toPosition.y - fromPosition.y, 0, window.innerHeight - this.frame.size.height),
1148
+ );
1149
+ this._displayConfiguration = this._createDisplayConfiguration();
1150
+ }
1151
+ },
1152
+
1153
+ // @override - BMWindow
1154
+ dragEndedAtPosition(position, {withEvent: event}) {
1155
+ BMWindow.prototype.dragEndedAtPosition.apply(this, arguments);
1156
+
1157
+ if (!this._isDetached && this._canDetach) {
1158
+ // If the drag operation didn't cause this popover to detach, animate it back to its
1159
+ // regular position
1160
+ BMAnimateWithBlock(() => {
1161
+ const controller = BMAnimationContextGetCurrent().controllerForObject(this, {node: this.node});
1162
+ controller.registerBuiltInPropertiesWithDictionary({
1163
+ translateX: '0px',
1164
+ translateY: '0px',
1165
+ });
1166
+ }, {duration: 500, easing: 'easeInOutQuart'});
1167
+ }
1168
+
1169
+ this._canDetach = NO;
1170
+ },
1171
+
1172
+ /**
1173
+ * Detaches this popover from its anchor and allows it to be freely movable.
1174
+ * @param animated <Boolean, nullable> Defaults to `YES`. When set to `YES`, this change will be animated,
1175
+ * otherwise it will be instant.
1176
+ * @returns <Promise<void>> A promise that resolves when the operation completes.
1177
+ */
1178
+ detachAnimated(animated) {
1179
+ // If this popover is already detached, this method has no effect
1180
+ if (this._isDetached) {
1181
+ return;
1182
+ }
1183
+
1184
+ // If this change is animated and there isn't already an active animation context, start one
1185
+ let animationContextStarted = NO;
1186
+ if (animated && !BMAnimationContextGetCurrent()) {
1187
+ animationContextStarted = YES;
1188
+ BMAnimationBeginWithDuration(300, {easing: 'easeInOutQuart'});
1189
+ }
1190
+
1191
+ this._isDetached = YES;
1192
+ this._displayConfiguration = this._createDisplayConfiguration();
1193
+ this._position = this.frame.origin.copy();
1194
+
1195
+ let promise;
1196
+
1197
+ // If a temporary transform was applied to this node, clear it
1198
+ if (BMAnimationContextGetCurrent()) {
1199
+ const controller = BMAnimationContextGetCurrent().controllerForObject(this, {node: this.node});
1200
+ controller.registerBuiltInPropertiesWithDictionary({
1201
+ translateX: '0px',
1202
+ translateY: '0px',
1203
+ });
1204
+
1205
+ promise = new Promise(r => BMAnimationContextAddCompletionHandler(r));
1206
+ }
1207
+ else {
1208
+ BMHook(this.node, {translateX: '0px', translateY: '0px'});
1209
+ }
1210
+
1211
+ if (animationContextStarted) {
1212
+ BMAnimationApplyBlocking(YES);
1213
+ }
1214
+
1215
+ if (!promise) {
1216
+ promise = Promise.resolve();
1217
+ }
1218
+
1219
+ return promise;
1220
+ },
1221
+
726
1222
  // @override - BMWindow
727
1223
  animateInWithCompletionHandler(completionHandler) {
728
1224
  const popoverLayers = [this.contentNode, this._background, this._dropShadowContainer];
@@ -763,7 +1259,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
763
1259
  bringToFrontAnimated(animated, args) {
764
1260
  if (!this.anchorNode && !this.anchorPoint && !this.anchorRect) throw new Error('The anchorPoint, anchorRect or anchorNode must be set prior to showing this popover.');
765
1261
 
766
- this._updatePosition();
1262
+ this._displayConfiguration = this._createDisplayConfiguration();
767
1263
 
768
1264
  BMWindow.prototype.bringToFrontAnimated.apply(this, arguments);
769
1265
  },
@@ -780,7 +1276,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
780
1276
  });
781
1277
 
782
1278
  /**
783
- * Constructs and returns a popover with the given size.
1279
+ * Constructs and returns a popover with the specified size.
784
1280
  * @param size <BMSize> The popover's size.
785
1281
  * @return <BMPopover> A popover.
786
1282
  */
@@ -788,4 +1284,28 @@ BMPopover.popoverWithSize = function (size) {
788
1284
  return (new BMPopover).initWithSize(size);
789
1285
  }
790
1286
 
1287
+ /**
1288
+ * Controls whether popovers retain the direction that was set to them when they were first
1289
+ * displayed, by default. When set to `NO`, whenever a popover's size or attributes change it will try
1290
+ * to find a new direction around the anchor. When set to `YES`, the current direction or the
1291
+ * initial display direction will be kept regardless of how the popover changes.
1292
+ */
1293
+ BMPopover._retainsDirection = NO; // <Boolean>
1294
+
1295
+ /**
1296
+ * Controls whether popovers retain the direction that was set to them when they were first
1297
+ * displayed, by default. When set to `NO`, whenever a popover's size or attributes change it will try
1298
+ * to find a new direction around the anchor. When set to `YES`, the current direction or the
1299
+ * initial display direction will be kept regardless of how the popover changes.
1300
+ * @param retains <Boolean, nullable> Defaults to `NO`. When set to `YES`, popovers will retain their
1301
+ * initial direction, otherwise they will recalculate their direction
1302
+ * whenever any update occurs.
1303
+ */
1304
+ BMPopover.setRetainsDirection = function (retains) {
1305
+ BMPopover._retainsDirection = retains || NO;
1306
+ }
1307
+
1308
+ // Set to YES for safari, which requires a reflow when the clip path is updated on some of the popover components
1309
+ BMPopover._requiresClipPathReflow = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
1310
+
791
1311
  // @endtype