bm-core-ui 2.11.7 → 2.11.9-beta.1

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
@@ -531,26 +823,85 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
531
823
  let transformOriginX, transformOriginY;
532
824
  switch (direction) {
533
825
  case BMPopoverIndicatorDirection.Bottom:
534
- transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
826
+ transformOriginX = ((indicatorPosition / frame.size.width) * 100) + '%';
535
827
  transformOriginY = '100%';
536
828
  break;
537
829
  case BMPopoverIndicatorDirection.Top:
538
- transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
830
+ transformOriginX = ((indicatorPosition / frame.size.width) * 100) + '%';
539
831
  transformOriginY = '0%';
540
832
  break;
541
833
  case BMPopoverIndicatorDirection.Left:
542
834
  transformOriginX = '0%';
543
- transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
835
+ transformOriginY = ((indicatorPosition / frame.size.height) * 100) + '%';
544
836
  break;
545
837
  case BMPopoverIndicatorDirection.Right:
546
838
  transformOriginX = '100%';
547
- transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
839
+ transformOriginY = ((indicatorPosition / frame.size.height) * 100) + '%';
548
840
  break;
549
841
  }
550
842
 
551
843
  for (const layer of popoverLayers) {
552
844
  layer.style.transformOrigin = `${transformOriginX} ${transformOriginY}`;
553
845
  }
846
+
847
+ this.frame = frame;
848
+ this.layoutIfNeeded();
849
+ },
850
+
851
+ /**
852
+ * The number of display configuration animations running currently against this popover's display configuration.
853
+ */
854
+ _displayConfigurationAnimations: 0, // <Number>
855
+
856
+ /**
857
+ * A display configuration that will be applied to this popover at the end of any current
858
+ * animations affecting the display configuration.
859
+ */
860
+ _pendingDisplayConfiguration: undefined, // <_BMPopoverDisplayConfiguration>
861
+
862
+ /**
863
+ * Animatable. Invoked by CoreUI to update this popover's position and recalculate the various paths used by it.
864
+ */
865
+ _updatePosition() {
866
+ // If the popover is not currently visible, there is not action to take
867
+ if (!this.isVisible) {
868
+ return;
869
+ }
870
+
871
+ const config = this._createDisplayConfiguration();
872
+
873
+ const context = BMAnimationContextGetCurrent();
874
+ if (context) {
875
+ const controller = context.controllerForObject(this, {node: this.node});
876
+ controller.registerAnimatableProperty('_displayConfiguration', {targetValue: config});
877
+
878
+ this._displayConfigurationAnimations++;
879
+ // When all animations finish on this popover, if there was any pending display configuration, apply it then
880
+ BMAnimationContextAddCompletionHandler(() => {
881
+ this._displayConfigurationAnimations--;
882
+
883
+ if (!this.isVisible) {
884
+ return;
885
+ }
886
+
887
+ if (!this._displayConfigurationAnimations && this._pendingDisplayConfiguration) {
888
+ BMAnimationContextBeginStatic(); {
889
+ this._displayConfiguration = this._pendingDisplayConfiguration;
890
+ this._pendingDisplayConfiguration = undefined;
891
+ } BMAnimationApplyBlocking();
892
+ }
893
+ });
894
+ }
895
+ else {
896
+ if (this._displayConfigurationAnimations) {
897
+ // If an animation is in progress, wait for it to finish before applying the new configuration
898
+ this._pendingDisplayConfiguration = config;
899
+ }
900
+ else {
901
+ // Else apply the configuration directly
902
+ this._displayConfiguration = config;
903
+ }
904
+ }
554
905
  },
555
906
 
556
907
  /**
@@ -723,6 +1074,138 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
723
1074
  return path;
724
1075
  },
725
1076
 
1077
+ /**
1078
+ * Controls whether an in-progress drag can detach this popover.
1079
+ */
1080
+ _canDetach: NO, // <Boolean>
1081
+
1082
+ /**
1083
+ * @protected
1084
+ * Controls whether this popover is detachable. Defaults to the result provided
1085
+ * by the delegate.
1086
+ * @return <Boolean> `YES` if the popover is detachable, `NO` otherwise.
1087
+ */
1088
+ isDetachable() {
1089
+ return this.delegate?.popoverCanDetach?.(this) ?? NO;
1090
+ },
1091
+
1092
+ /**
1093
+ * During a drag operation set to the point where the drag began.
1094
+ */
1095
+ _initialDragPosition: undefined, // <BMPoint, nullable>
1096
+
1097
+ // @override - BMWindow
1098
+ dragBeganAtPosition(position, {withEvent: event}) {
1099
+ this._canDetach = this.isDetachable();
1100
+
1101
+ // If the popover is neither detached nor can it detach, drags cannot be performed
1102
+ if (!this._canDetach && !this._isDetached) {
1103
+ return;
1104
+ }
1105
+
1106
+ this._initialDragPosition = position.copy();
1107
+ BMWindow.prototype.dragBeganAtPosition.apply(this, arguments);
1108
+ },
1109
+
1110
+ // @override - BMWindow
1111
+ dragPositionDidChangeFromPosition(fromPosition, {toPosition, event}) {
1112
+ if (!this._canDetach && !this._isDetached) {
1113
+ return;
1114
+ }
1115
+
1116
+ if (!this._isDetached) {
1117
+ // If not detached, apply a displacement to this popover that is half of the regular
1118
+ // drag distance, until this popover is fully detached
1119
+ BMHook(this.node, {
1120
+ translateX: `${(toPosition.x - this._initialDragPosition.x) / 3 | 0}px`,
1121
+ translateY: `${(toPosition.y - this._initialDragPosition.y) / 3 | 0}px`,
1122
+ });
1123
+
1124
+ const distance = toPosition.distanceToPoint(this._initialDragPosition);
1125
+ if (distance > BMPopoverDragThreshold) {
1126
+ // If the movement exceeds the detachment threshold, detach this popover
1127
+ this.detachAnimated(YES);
1128
+ }
1129
+ }
1130
+ else {
1131
+ // If this is detached, update the popover's position
1132
+ this._position = BMPointMake(
1133
+ BMNumberByConstrainingNumberToBounds(this._position.x + toPosition.x - fromPosition.x, 0, window.innerWidth - this.frame.size.width),
1134
+ BMNumberByConstrainingNumberToBounds(this._position.y + toPosition.y - fromPosition.y, 0, window.innerHeight - this.frame.size.height),
1135
+ );
1136
+ this._displayConfiguration = this._createDisplayConfiguration();
1137
+ }
1138
+ },
1139
+
1140
+ // @override - BMWindow
1141
+ dragEndedAtPosition(position, {withEvent: event}) {
1142
+ BMWindow.prototype.dragEndedAtPosition.apply(this, arguments);
1143
+
1144
+ if (!this._isDetached && this._canDetach) {
1145
+ // If the drag operation didn't cause this popover to detach, animate it back to its
1146
+ // regular position
1147
+ BMAnimateWithBlock(() => {
1148
+ const controller = BMAnimationContextGetCurrent().controllerForObject(this, {node: this.node});
1149
+ controller.registerBuiltInPropertiesWithDictionary({
1150
+ translateX: '0px',
1151
+ translateY: '0px',
1152
+ });
1153
+ }, {duration: 500, easing: 'easeInOutQuart'});
1154
+ }
1155
+
1156
+ this._canDetach = NO;
1157
+ },
1158
+
1159
+ /**
1160
+ * Detaches this popover from its anchor and allows it to be freely movable.
1161
+ * @param animated <Boolean, nullable> Defaults to `YES`. When set to `YES`, this change will be animated,
1162
+ * otherwise it will be instant.
1163
+ * @returns <Promise<void>> A promise that resolves when the operation completes.
1164
+ */
1165
+ detachAnimated(animated) {
1166
+ // If this popover is already detached, this method has no effect
1167
+ if (this._isDetached) {
1168
+ return;
1169
+ }
1170
+
1171
+ // If this change is animated and there isn't already an active animation context, start one
1172
+ let animationContextStarted = NO;
1173
+ if (animated && !BMAnimationContextGetCurrent()) {
1174
+ animationContextStarted = YES;
1175
+ BMAnimationBeginWithDuration(300, {easing: 'easeInOutQuart'});
1176
+ }
1177
+
1178
+ this._isDetached = YES;
1179
+ this._displayConfiguration = this._createDisplayConfiguration();
1180
+ this._position = this.frame.origin.copy();
1181
+
1182
+ let promise;
1183
+
1184
+ // If a temporary transform was applied to this node, clear it
1185
+ if (BMAnimationContextGetCurrent()) {
1186
+ const controller = BMAnimationContextGetCurrent().controllerForObject(this, {node: this.node});
1187
+ controller.registerBuiltInPropertiesWithDictionary({
1188
+ translateX: '0px',
1189
+ translateY: '0px',
1190
+ });
1191
+
1192
+ promise = new Promise(r => BMAnimationContextAddCompletionHandler(r));
1193
+ }
1194
+ else {
1195
+ BMHook(this.node, {translateX: '0px', translateY: '0px'});
1196
+ }
1197
+
1198
+ if (animationContextStarted) {
1199
+ BMAnimationApplyBlocking(YES);
1200
+ }
1201
+
1202
+ if (!promise) {
1203
+ promise = Promise.resolve();
1204
+ }
1205
+
1206
+ return promise;
1207
+ },
1208
+
726
1209
  // @override - BMWindow
727
1210
  animateInWithCompletionHandler(completionHandler) {
728
1211
  const popoverLayers = [this.contentNode, this._background, this._dropShadowContainer];
@@ -763,7 +1246,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
763
1246
  bringToFrontAnimated(animated, args) {
764
1247
  if (!this.anchorNode && !this.anchorPoint && !this.anchorRect) throw new Error('The anchorPoint, anchorRect or anchorNode must be set prior to showing this popover.');
765
1248
 
766
- this._updatePosition();
1249
+ this._displayConfiguration = this._createDisplayConfiguration();
767
1250
 
768
1251
  BMWindow.prototype.bringToFrontAnimated.apply(this, arguments);
769
1252
  },
@@ -780,7 +1263,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
780
1263
  });
781
1264
 
782
1265
  /**
783
- * Constructs and returns a popover with the given size.
1266
+ * Constructs and returns a popover with the specified size.
784
1267
  * @param size <BMSize> The popover's size.
785
1268
  * @return <BMPopover> A popover.
786
1269
  */
@@ -788,4 +1271,25 @@ BMPopover.popoverWithSize = function (size) {
788
1271
  return (new BMPopover).initWithSize(size);
789
1272
  }
790
1273
 
1274
+ /**
1275
+ * Controls whether popovers retain the direction that was set to them when they were first
1276
+ * displayed, by default. When set to `NO`, whenever a popover's size or attributes change it will try
1277
+ * to find a new direction around the anchor. When set to `YES`, the current direction or the
1278
+ * initial display direction will be kept regardless of how the popover changes.
1279
+ */
1280
+ BMPopover._retainsDirection = NO; // <Boolean>
1281
+
1282
+ /**
1283
+ * Controls whether popovers retain the direction that was set to them when they were first
1284
+ * displayed, by default. When set to `NO`, whenever a popover's size or attributes change it will try
1285
+ * to find a new direction around the anchor. When set to `YES`, the current direction or the
1286
+ * initial display direction will be kept regardless of how the popover changes.
1287
+ * @param retains <Boolean, nullable> Defaults to `NO`. When set to `YES`, popovers will retain their
1288
+ * initial direction, otherwise they will recalculate their direction
1289
+ * whenever any update occurs.
1290
+ */
1291
+ BMPopover.setRetainsDirection = function (retains) {
1292
+ BMPopover._retainsDirection = retains || NO;
1293
+ }
1294
+
791
1295
  // @endtype