bm-core-ui 2.8.3 → 2.9.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.
@@ -103,7 +103,7 @@ BMScrollView.prototype = BMExtend({}, BMView.prototype, {
103
103
  },
104
104
 
105
105
  /**
106
- * Invoked whenever the iScroll instance managing this scroll view's scrolling scrolls.
106
+ * Invoked whenever the iScroll instance managing this scroll view's scrolling.
107
107
  */
108
108
  _iScrollDidScroll() {
109
109
  this._scrollOffset = BMPointMake(this._iScroll.x, this._iScroll.y);
@@ -1,7 +1,7 @@
1
1
  // @type interface BMTextFieldDelegate
2
2
 
3
3
  /**
4
- * The specification for a `BMTextField` object, which can optionally be used in conjunction with `BMTextField` objects and will receive various
4
+ * The specification for a `BMTextFieldDelegate` object, which can optionally be used in conjunction with `BMTextField` objects and will receive various
5
5
  * callbacks related to the text field's lifecycle.
6
6
  */
7
7
  function BMTextFieldDelegate() {} // <constructor>
@@ -15,7 +15,7 @@ BMTextFieldDelegate.prototype = {
15
15
  *
16
16
  * If this method is not implemented, suggestions and autocomplete will not be available.
17
17
  * @param textField <BMTextField> The calling text field.
18
- * @param text <String>
18
+ * @param text <String> The text for which to obtain suggestions.
19
19
  * @return <[String]> An array of string suggestions.
20
20
  */
21
21
  textFieldSuggestionsForText(textField, text) {},
@@ -47,7 +47,7 @@ BMTextFieldDelegate.prototype = {
47
47
  /**
48
48
  * Invoked whenever the contents in this text field change for any reason. This can be because of an input event,
49
49
  * the user pasting text or a suggestion being selected.
50
- * Note that this method will not be invoked when manually setting the input node's `value`.
50
+ * Note that this method will not be invoked when programatically setting the input node's `value`.
51
51
  *
52
52
  * Delegate objects can implement this method in place of standard event listeners to handle changes to this text field.
53
53
  * The contents of the text field can be retrieved via the standard `value` property on the text field's node.
@@ -58,9 +58,9 @@ BMTextFieldDelegate.prototype = {
58
58
  /**
59
59
  * Invoked whenever the user presses the return key while the text field has keyboard focus.
60
60
  *
61
- * Delegate objects can implement this method to customize the behaviour when the user presses the return key and also control whether the default action will take place.
62
- * The default action for pressing the return key is to cause the text field to resign keyboard focus. If suggestions are used, the return key will also fill in the currently
63
- * highlighted suggestion.
61
+ * Delegate objects can implement this method to customize the behaviour when the user presses the return key and also control
62
+ * whether the default action will take place. The default action for pressing the return key is to cause the text field to
63
+ * resign keyboard focus. If suggestions are used, the return key will also fill in the currently highlighted suggestion.
64
64
  * @param textField <BMTextField> The calling text field.
65
65
  * @return <Boolean, nullable> Defaults to `YES`. If set to `YES`, the standard behaviour will be invoked.
66
66
  */
@@ -0,0 +1,101 @@
1
+ //@ts-check
2
+
3
+ //@type BMViewShadow
4
+
5
+ import { BMCopyProperties, YES } from "../Core/BMCoreUI";
6
+ import { BMRectMakeWithNodeFrame } from "../Core/BMRect";
7
+
8
+ /**
9
+ * A view shadow represents a copy of a view's DOM nodes, without any event handlers attached
10
+ * that is always positioned absolutely. View shadows are created by invoking `retainShadow` on a view
11
+ * and are typically used for animations where it is needed for the view to move outside of its
12
+ * container.
13
+ *
14
+ * A view may only have a single shadow active at a time.
15
+ */
16
+ export class BMViewShadow {
17
+
18
+ /**
19
+ * The DOM node rendered by this view shadow.
20
+ */
21
+ _node; // <DOMNode>
22
+
23
+ get node() {
24
+ return this._node;
25
+ }
26
+
27
+ /**
28
+ * Animatable. The view shadow's frame, relative to the viewport.
29
+ */
30
+ _frame; // <BMRect>
31
+
32
+ get frame() {
33
+ return this._frame;
34
+ }
35
+
36
+ set frame(frame) {
37
+ this._frame = frame.copy();
38
+
39
+ BMCopyProperties(this._node, {
40
+ left: `${frame.left}px`,
41
+ top: `${frame.top}px`,
42
+ width: `${frame.width}px`,
43
+ height: `${frame.height}px`
44
+ });
45
+ }
46
+
47
+ /**
48
+ * The shadow's retain count.
49
+ */
50
+ _retainCount = 0; // <Number>
51
+
52
+ /**
53
+ * Initializes this view shadow by copying the DOM content of
54
+ * the specified view.
55
+ * @param view <BMView> The view whose DOM content should be copied.
56
+ * @returns <BMViewShadow> This view shadow.
57
+ */
58
+ initWithView(view) {
59
+ this._node = view.node.copy(YES);
60
+
61
+ this._node.classList.add('BMViewShadow');
62
+
63
+ // Make this node shadow occupy the same position as the original node
64
+ const frame = BMRectMakeWithNodeFrame(view.node);
65
+
66
+ // In scrolling containers, the view may often get cut off, which will cause
67
+ // incorrect sizes to be reported by the bounding client rect
68
+ frame.size.width = view.node.offsetWidth;
69
+ frame.size.height = view.node.offsetHeight;
70
+
71
+ this.frame = frame;
72
+ }
73
+
74
+ /**
75
+ * Retains this view shadow, increasing its retain count by 1.
76
+ *
77
+ * This method must be invoked before this view shadow is used as part of any animation
78
+ * or interaction. When it is no longer needed, the implementation should invoke the
79
+ * `release` method.
80
+ */
81
+ retain() {
82
+ this._retainCount++;
83
+ }
84
+
85
+ /**
86
+ * Releases this view shadow, decreasing its retain count by 1.
87
+ *
88
+ * This method must be invoked whenever this view shadow is no longer needed.
89
+ * When the retain count reaches 0, this view shadow will be removed from the document.
90
+ */
91
+ release() {
92
+ this._retainCount--;
93
+
94
+ if (!this._retainCount) {
95
+ this._node.remove();
96
+ }
97
+ }
98
+
99
+ }
100
+
101
+ //@endtype
@@ -30,8 +30,11 @@ _BMLayoutEditorViewSettingsPanel.prototype = BMExtend(Object.create(_BMLayoutEdi
30
30
  _layoutTab: undefined, // <BMLayoutEditorSettingsTab>
31
31
 
32
32
  /**
33
- * Designated initializer. Initializes this settings panel with the given settings view.
34
- * @param view <_BMLayoutSettingsView> The settings view.
33
+ * Designated initializer. Initializes this settings panel with the specified settings view.
34
+ * @param settingsView <_BMLayoutSettingsView> The settings view.
35
+ * {
36
+ * @param forView <BMView> The view whose settings will be displayed.
37
+ * }
35
38
  * @return <_BMLayoutEditorSettingsPanel> This setttings panel.
36
39
  */
37
40
  initWithSettingsView(settingsView, {forView: view}) {
@@ -182,7 +182,12 @@ BMAlertPopup.prototype = BMExtend(Object.create(BMWindow.prototype), {
182
182
  /**
183
183
  * The DOM node that was focused when this alert was opened.
184
184
  */
185
- _previouslyActiveNode: undefined, // DOMNode
185
+ _previouslyActiveNode: undefined, // <DOMNode>
186
+
187
+ /**
188
+ * A handler that is invoked when the window resizes, used to reposition this window.
189
+ */
190
+ _centerHandler: undefined, // <void ^()>
186
191
 
187
192
  /**
188
193
  * Designated initializer. Initializes this alert popup with the given labels.
@@ -290,6 +295,15 @@ BMAlertPopup.prototype = BMExtend(Object.create(BMWindow.prototype), {
290
295
  return this._resolution;
291
296
  },
292
297
 
298
+ // @override - BMWindow
299
+ bringToFrontAnimated() {
300
+ this._resizeHandler = () => this.needsLayout = YES;
301
+
302
+ window.addEventListener('resize', this._resizeHandler);
303
+
304
+ return BMWindow.prototype.bringToFrontAnimated.apply(this, arguments);
305
+ },
306
+
293
307
  // @override - BMWindow
294
308
  dismissAnimated() {
295
309
  // Since the window can be dismissed by clicking outside in certain cases, this would be equivalent to pressing "OK".
@@ -302,7 +316,13 @@ BMAlertPopup.prototype = BMExtend(Object.create(BMWindow.prototype), {
302
316
  this._previouslyActiveNode.focus();
303
317
  }
304
318
 
319
+ window.removeEventListener('resize', this._resizeHandler);
320
+
305
321
  return BMWindow.prototype.dismissAnimated.apply(this, arguments);
322
+ },
323
+
324
+ windowDidClose() {
325
+ this.release();
306
326
  }
307
327
 
308
328
  });
@@ -8,6 +8,35 @@ import { BMHook, __BMVelocityAnimate } from "../../Core/BMAnimationContext";
8
8
  import { BMView } from "../../BMView/BMView_v2.5";
9
9
  import { BMInsetMakeWithEqualInsets } from "../../Core/BMInset";
10
10
 
11
+ // @type BMPopoverIndicatorDirection
12
+
13
+ // @endtype
14
+
15
+ /**
16
+ * Constants describing the position where the indicator appears on its popover.
17
+ */
18
+ export var BMPopoverIndicatorDirection = Object.freeze({ // <enum>
19
+ /**
20
+ * Causes the popover indicator to appear on the top edge of the popover.
21
+ */
22
+ Top: "Top", // <enum>
23
+
24
+ /**
25
+ * Causes the popover indicator to appear on the bottom edge of the popover.
26
+ */
27
+ Bottom: "Bottom", // <enum>
28
+
29
+ /**
30
+ * Causes the popover indicator to appear on the left edge of the popover.
31
+ */
32
+ Left: "Left", // <enum>
33
+
34
+ /**
35
+ * Causes the popover indicator to appear on the right edge of the popover.
36
+ */
37
+ Right: "Right" // <enum>
38
+ });
39
+
11
40
  // @type BMPopover extends BMWindow
12
41
 
13
42
  /**
@@ -90,7 +119,7 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
90
119
  * Controls how rounded the popover's borders should be. This property should be set prior to the
91
120
  * popover being displayed.
92
121
  */
93
- _borderRadius: 4, // <Number>
122
+ _borderRadius: 8, // <Number>
94
123
  get borderRadius() {
95
124
  return this._borderRadius;
96
125
  },
@@ -182,6 +211,16 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
182
211
  */
183
212
  _contentViewBottomConstraint: undefined, // <BMLayoutConstraint>
184
213
 
214
+ /**
215
+ * The content view's left edge constraint.
216
+ */
217
+ _contentViewLeftConstraint: undefined, // <BMLayoutConstraint>
218
+
219
+ /**
220
+ * The content view's right edge constraint.
221
+ */
222
+ _contentViewRightConstraint: undefined, // <BMLayoutConstraint>
223
+
185
224
  /**
186
225
  * The computed height of the indicator.
187
226
  */
@@ -190,6 +229,18 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
190
229
  return indicatorWidth / 2 | 0;
191
230
  },
192
231
 
232
+ /**
233
+ * An array that specifies the permitted indicator directions that this popover
234
+ * may use and the priority in which they will be evaluated.
235
+ */
236
+ _permittedDirections: [BMPopoverIndicatorDirection.Bottom, BMPopoverIndicatorDirection.Top, BMPopoverIndicatorDirection.Right, BMPopoverIndicatorDirection.Left], // <[BMPopoverIndicatorDirection]>
237
+ get permittedDirections() {
238
+ return this._permittedDirections.slice();
239
+ },
240
+ set permittedDirections(directions) {
241
+ this._permittedDirections = directions.slice();
242
+ },
243
+
193
244
  /**
194
245
  * Designated initializer. Initializes this popover with the given size.
195
246
  *
@@ -239,8 +290,12 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
239
290
  const contentView = this._contentView = BMView.view();
240
291
  this.addSubview(this._contentView);
241
292
 
242
- contentView.leading.equalTo(this.leading).isActive = YES;
243
- contentView.trailing.equalTo(this.trailing).isActive = YES;
293
+ this._contentViewLeftConstraint = contentView.left.equalTo(this.left);
294
+ this._contentViewLeftConstraint.isActive = YES;
295
+
296
+ this._contentViewRightConstraint = contentView.right.equalTo(this.right);
297
+ this._contentViewRightConstraint.isActive = YES;
298
+
244
299
  this._contentViewTopConstraint = contentView.top.equalTo(this.top);
245
300
  this._contentViewTopConstraint.isActive = YES;
246
301
 
@@ -266,38 +321,108 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
266
321
  const nodeFrame = this.anchorRect || (this.anchorNode && BMRectMakeWithNodeFrame(this.anchorNode));
267
322
  const location = this.anchorPoint ? this.anchorPoint.copy() : nodeFrame.center;
268
323
 
269
- frame.origin.x = location.x - frame.size.width / 2 | 0;
270
-
271
- if (frame.origin.x < this._edgeInsets.left) {
272
- frame.origin.x = this._edgeInsets.left;
324
+ // Determine the appropriate direction to display this popover
325
+ let direction;
326
+ if (this.anchorPoint) {
327
+ direction = this._directionAroundPoint(location);
273
328
  }
274
- if (frame.right > window.innerWidth - this._edgeInsets.right) {
275
- frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
329
+ else {
330
+ direction = this._directionAroundRect(nodeFrame);
276
331
  }
277
332
 
278
- const appearsBelow = location.y < window.innerHeight - this.size.height - this._indicatorHeight;
333
+ // Adjust the constraints based on the direction
334
+ switch (direction) {
335
+ case BMPopoverIndicatorDirection.Top:
336
+ frame.origin.y = this.anchorPoint ? location.y : nodeFrame.bottom - 2;
279
337
 
280
- if (appearsBelow) {
281
- frame.origin.y = this.anchorPoint ? location.y : nodeFrame.bottom - 2;
282
- this._contentViewTopConstraint.constant = this._indicatorHeight;
283
- this._contentViewBottomConstraint.constant = 0;
284
- }
285
- else {
286
- frame.origin.y = this.anchorPoint ? location.y - frame.size.height : nodeFrame.origin.y + 2 - frame.size.height;
287
- this._contentViewTopConstraint.constant = 0;
288
- this._contentViewBottomConstraint.constant = -this._indicatorHeight;
338
+ frame.origin.x = location.x - frame.size.width / 2 | 0;
339
+
340
+ if (frame.origin.x < this._edgeInsets.left) {
341
+ frame.origin.x = this._edgeInsets.left;
342
+ }
343
+ if (frame.right > window.innerWidth - this._edgeInsets.right) {
344
+ frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
345
+ }
346
+
347
+ this._contentViewTopConstraint.constant = this._indicatorHeight;
348
+ this._contentViewBottomConstraint.constant = 0;
349
+ this._contentViewLeftConstraint.constant = 0;
350
+ this._contentViewRightConstraint.constant = 0;
351
+ break;
352
+ case BMPopoverIndicatorDirection.Bottom:
353
+ frame.origin.y = this.anchorPoint ? location.y - frame.size.height : nodeFrame.origin.y + 2 - frame.size.height;
354
+
355
+ frame.origin.x = location.x - frame.size.width / 2 | 0;
356
+
357
+ if (frame.origin.x < this._edgeInsets.left) {
358
+ frame.origin.x = this._edgeInsets.left;
359
+ }
360
+ if (frame.right > window.innerWidth - this._edgeInsets.right) {
361
+ frame.origin.x = window.innerWidth - frame.size.width - this._edgeInsets.right;
362
+ }
363
+
364
+ this._contentViewTopConstraint.constant = 0;
365
+ this._contentViewBottomConstraint.constant = -this._indicatorHeight;
366
+ this._contentViewLeftConstraint.constant = 0;
367
+ this._contentViewRightConstraint.constant = 0;
368
+ break;
369
+ case BMPopoverIndicatorDirection.Right:
370
+ frame.origin.x = this.anchorPoint ? location.x - frame.size.width : nodeFrame.origin.x + 2 - frame.size.width;
371
+
372
+ frame.origin.y = location.y - frame.size.height / 2 | 0;
373
+
374
+ if (frame.origin.y < this._edgeInsets.top) {
375
+ frame.origin.y = this._edgeInsets.top;
376
+ }
377
+ if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
378
+ frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
379
+ }
380
+
381
+ this._contentViewTopConstraint.constant = 0;
382
+ this._contentViewBottomConstraint.constant = 0;
383
+ this._contentViewLeftConstraint.constant = 0;
384
+ this._contentViewRightConstraint.constant = -this._indicatorHeight;
385
+ break;
386
+ case BMPopoverIndicatorDirection.Left:
387
+ frame.origin.x = this.anchorPoint ? location.x : nodeFrame.right - 2;
388
+
389
+ frame.origin.y = location.y - frame.size.height / 2 | 0;
390
+
391
+ if (frame.origin.y < this._edgeInsets.top) {
392
+ frame.origin.y = this._edgeInsets.top;
393
+ }
394
+ if (frame.bottom > window.innerHeight - this._edgeInsets.bottom) {
395
+ frame.origin.y = window.innerHeight - frame.size.height - this._edgeInsets.bottom;
396
+ }
397
+
398
+ this._contentViewTopConstraint.constant = 0;
399
+ this._contentViewBottomConstraint.constant = 0;
400
+ this._contentViewLeftConstraint.constant = this._indicatorHeight;
401
+ this._contentViewRightConstraint.constant = 0;
402
+ break;
289
403
  }
290
404
 
291
405
  this.frame = frame;
292
406
 
293
- const innerFrame = frame.copy();
294
-
295
- const knobPosition = BMNumberByConstrainingNumberToBounds(location.x - frame.origin.x, 12, frame.size.width - 12);
407
+ // Determine the indicator's position along its edge
408
+ let indicatorPosition;
409
+ switch (direction) {
410
+ case BMPopoverIndicatorDirection.Bottom:
411
+ case BMPopoverIndicatorDirection.Top:
412
+ indicatorPosition = BMNumberByConstrainingNumberToBounds(location.x - frame.origin.x, 12, frame.size.width - 12);
413
+ break;
414
+ case BMPopoverIndicatorDirection.Left:
415
+ case BMPopoverIndicatorDirection.Right:
416
+ indicatorPosition = BMNumberByConstrainingNumberToBounds(location.y - frame.origin.y, 12, frame.size.height - 12);
417
+ break;
418
+ }
296
419
 
420
+ // Create an inner frame to be used by the various paths
421
+ const innerFrame = frame.copy();
297
422
  innerFrame.origin = BMPointMake();
298
423
 
299
- const pathContent = `${this._pathForPopoverWithFrame(innerFrame, {widthIndicatorSize: this._indicatorSize, knobPosition, gravity: appearsBelow ? 'Top' : 'Bottom'})}`;
300
- const outlinePathContent = `${this._pathForPopoverWithFrame(innerFrame, {widthIndicatorSize: this._indicatorSize, inset: 1, knobPosition, gravity: appearsBelow ? 'Top' : 'Bottom'})}`;
424
+ const pathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize: this._indicatorSize, position: indicatorPosition, direction, radius: this._borderRadius})}`;
425
+ const outlinePathContent = `${this._pathForPopoverWithFrame(innerFrame, {indicatorSize: this._indicatorSize, inset: 1, position: indicatorPosition, direction, radius: this._borderRadius})}`;
301
426
 
302
427
  if (!this._clipPathUUID && !CSS.supports('clip-path', `path('${pathContent}')`)) {
303
428
  // If inline path definitions are not supported by the browsers, create an UUID for a SVG clip path and create it
@@ -362,64 +487,197 @@ BMPopover.prototype = BMExtend(Object.create(BMWindow.prototype), {
362
487
 
363
488
  const popoverLayers = [this.contentNode, this._background, this._dropShadowContainer];
364
489
 
490
+ let transformOriginX, transformOriginY;
491
+ switch (direction) {
492
+ case BMPopoverIndicatorDirection.Bottom:
493
+ transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
494
+ transformOriginY = '100%';
495
+ break;
496
+ case BMPopoverIndicatorDirection.Top:
497
+ transformOriginX = ((indicatorPosition / this.frame.size.width) * 100) + '%';
498
+ transformOriginY = '0%';
499
+ break;
500
+ case BMPopoverIndicatorDirection.Left:
501
+ transformOriginX = '0%';
502
+ transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
503
+ break;
504
+ case BMPopoverIndicatorDirection.Right:
505
+ transformOriginX = '100%';
506
+ transformOriginY = ((indicatorPosition / this.frame.size.height) * 100) + '%';
507
+ break;
508
+ }
509
+
365
510
  for (const layer of popoverLayers) {
366
- layer.style.transformOrigin = ((knobPosition / this.frame.size.width) * 100) + '% ' + (appearsBelow ? '0%' : '100%');
511
+ layer.style.transformOrigin = `${transformOriginX} ${transformOriginY}`;
367
512
  }
368
513
  },
369
514
 
515
+ /**
516
+ * Determines the direction that the popover should appear in order to fit best around the given point.
517
+ * The popover will verify directions in the order specified by the `permittedDirections` property.
518
+ * If none of the permitted indicator directions would fit the popover in the viewport, the first
519
+ * specified direction is returned.
520
+ * @param point <BMPoint> The anchor point to check against.
521
+ * @return <BMPopoverIndicatorDirection> The direction that best fits.
522
+ */
523
+ _directionAroundPoint(point) {
524
+ for (const direction of this._permittedDirections) {
525
+ switch (direction) {
526
+ case BMPopoverIndicatorDirection.Top:
527
+ if (point.y < window.innerHeight - this.size.height - this._indicatorHeight) {
528
+ return BMPopoverIndicatorDirection.Top;
529
+ }
530
+ break;
531
+ case BMPopoverIndicatorDirection.Bottom:
532
+ if (point.y > this.size.height + this._indicatorHeight) {
533
+ return BMPopoverIndicatorDirection.Bottom;
534
+ }
535
+ break;
536
+ case BMPopoverIndicatorDirection.Right:
537
+ if (point.x > this.size.width + this._indicatorHeight) {
538
+ return BMPopoverIndicatorDirection.Right;
539
+ }
540
+ break;
541
+ case BMPopoverIndicatorDirection.Left:
542
+ if (point.x < window.innerWidth - this.size.width - this._indicatorHeight) {
543
+ return BMPopoverIndicatorDirection.Left;
544
+ }
545
+ break;
546
+ }
547
+ }
548
+
549
+ return this._permittedDirections[0];
550
+ },
551
+
552
+ /**
553
+ * Determines the direction that the popover should appear in order to fit best around the given rect.
554
+ * The popover will verify directions in the order specified by the `permittedDirections` property.
555
+ * If none of the permitted indicator directions would fit the popover in the viewport, the first
556
+ * specified direction is returned.
557
+ * @param rect <BMRect> The anchor rect to check against.
558
+ * @return <BMPopoverIndicatorDirection> The direction that best fits.
559
+ */
560
+ _directionAroundRect(rect) {
561
+ for (const direction of this._permittedDirections) {
562
+ switch (direction) {
563
+ case BMPopoverIndicatorDirection.Top:
564
+ if (rect.bottom < window.innerHeight - this.size.height - this._indicatorHeight) {
565
+ return BMPopoverIndicatorDirection.Top;
566
+ }
567
+ break;
568
+ case BMPopoverIndicatorDirection.Bottom:
569
+ if (rect.top > this.size.height + this._indicatorHeight) {
570
+ return BMPopoverIndicatorDirection.Bottom;
571
+ }
572
+ break;
573
+ case BMPopoverIndicatorDirection.Right:
574
+ if (rect.left > this.size.width + this._indicatorHeight) {
575
+ return BMPopoverIndicatorDirection.Right;
576
+ }
577
+ break;
578
+ case BMPopoverIndicatorDirection.Left:
579
+ if (rect.right < window.innerWidth - this.size.width - this._indicatorHeight) {
580
+ return BMPopoverIndicatorDirection.Left;
581
+ }
582
+ break;
583
+ }
584
+ }
585
+
586
+ return this._permittedDirections[0];
587
+ },
588
+
370
589
  /**
371
590
  * Builds the SVG path definition for a popover with the given frame. Note that the indicator will be positioned inside the frame, which will push the usable
372
591
  * area of the frame downwards.
373
- * @param frame <BMRect> The popover's frame.
592
+ * @param frame <BMRect> The popover's frame.
374
593
  * {
375
- * @param widthIndicatorSize <Number, nullable> Defaults to `8`. The size of the popover's indicator.
376
- * @param radius <Number, nullable> Defaults to `4`. Controls how rounded the corners are.
377
- * @param inset <Number, nullable> Defaults to `0`. An optional inset to apply to the path.
378
- * @param knobPosition <Number, nullable> Defaults to half of the frame's width. The position along the top frame on which to place the knob.
379
- * This coordinate is relative to the popover's frame and represents the center position of the knob.
380
- * This position should not overlap the specified corner radius.
381
- * @param gravity <String, nullable> Defaults to `"Top"`. If set to `"Bottom"`, the popover knob will be placed at the bottom of the popover.
594
+ * @param indicatorSize <Number, nullable> Defaults to `8`. The size of the popover's indicator.
595
+ * @param radius <Number, nullable> Defaults to `4`. Controls how rounded the corners are.
596
+ * @param inset <Number, nullable> Defaults to `0`. An optional inset to apply to the path.
597
+ * @param position <Number, nullable> Defaults to half of the frame's width. The position along the edge of the frame on which to place the indicator.
598
+ * This coordinate is relative to the popover's frame and represents the center position of the indicator.
599
+ * This position should not overlap the specified corner radius.
600
+ * @param direction <BMPopoverIndicatorDirection, nullable> Defaults to `.Top`. Controls where the indicator will be placed relative to the popover.
382
601
  * }
383
602
  * @return <String> The SVG path.
384
603
  */
385
- _pathForPopoverWithFrame(frame, {widthIndicatorSize: size = 8, radius = 8, inset = 0, knobPosition = undefined, gravity = 'Top'} = {widthIndicatorSize: 8, radius: 4, inset: 0}) {
386
- let top = gravity === 'Bottom' ? 0 : size * Math.SQRT2 / 2 | 0;
387
- let bottom = gravity === 'Bottom' ? frame.size.height - size * Math.SQRT2 / 2 | 0 : frame.size.height;
388
- const left = inset;
604
+ _pathForPopoverWithFrame(frame, {indicatorSize = 8, radius = 8, inset = 0, position = undefined, direction = BMPopoverIndicatorDirection.Top} = {indicatorSize: 8, radius: 4, inset: 0}) {
605
+ // Adjust the appropriate edge's position depending on the indicator direction
606
+ let top = direction === BMPopoverIndicatorDirection.Top ? indicatorSize * Math.SQRT2 / 2 | 0 : 0;
607
+ let left = direction == BMPopoverIndicatorDirection.Left ? indicatorSize * Math.SQRT2 / 2 | 0 : 0;
608
+
609
+ let bottom = direction === BMPopoverIndicatorDirection.Bottom ? frame.size.height - indicatorSize * Math.SQRT2 / 2 | 0 : frame.size.height;
610
+ let right = direction == BMPopoverIndicatorDirection.Right ? frame.size.width - indicatorSize * Math.SQRT2 / 2 | 0 : frame.size.width;
389
611
 
390
612
  if (inset) {
391
- size = size - inset;
613
+ // If an inset is specified, adjust the values appropriately
614
+ indicatorSize = indicatorSize - inset;
392
615
  frame = frame.copy();
393
616
  frame.insetWithInset(BMInsetMakeWithEqualInsets(inset));
394
617
  radius = radius + inset;
618
+
395
619
  top += inset;
620
+ left += inset;
621
+
396
622
  bottom -= inset;
623
+ right -= inset;
397
624
  }
398
625
 
399
- if (knobPosition === undefined) {
400
- knobPosition = frame.size.width / 2 | 0;
626
+ if (position === undefined) {
627
+ position = frame.size.width / 2 | 0;
401
628
  }
402
629
  else {
403
- knobPosition = (knobPosition - inset) | 0;
630
+ position = (position - inset) | 0;
404
631
  }
405
632
 
406
- const knobWidth = size * Math.SQRT2;
633
+ const knobWidth = indicatorSize * Math.SQRT2;
407
634
  const knobHeight = knobWidth / 2 | 0;
408
635
 
409
- // bottom should be equivalent to frame.size.height + inset
410
- const pathTop = gravity === 'Bottom' ?
411
- `M${left + radius},${top} L${frame.size.width + left - radius},${top} ` :
412
- `M${left + radius},${top} L${(knobPosition - knobWidth / 2 + left)},${top} l${(knobWidth / 2)},${-knobHeight} l${(knobWidth / 2)},${knobHeight} L${frame.size.width + left - radius},${top} `;
413
-
414
- const pathBottom = gravity === 'Bottom' ?
415
- `Q${frame.size.width + left},${bottom} ${frame.size.width + left - radius},${bottom} L${(knobPosition + knobWidth / 2 + left)},${bottom} l${(-knobWidth / 2)},${knobHeight} l${(-knobWidth / 2)},${-knobHeight} L${radius},${bottom} ` :
416
- `Q${frame.size.width + left},${bottom} ${frame.size.width + left - radius},${bottom} L${radius},${bottom} `;
636
+ const pathTop = direction === BMPopoverIndicatorDirection.Top ?
637
+ // Start at top left, then draw the indicator and move to the end of the edge
638
+ `M${left + radius},${top} ` +
639
+ `L${(position - knobWidth / 2 + left)},${top} l${(knobWidth / 2)},${-knobHeight} l${(knobWidth / 2)},${knobHeight} ` +
640
+ `L${right - radius},${top} ` :
641
+ // Start at top left, then move to the end of the edge
642
+ `M${left + radius},${top} ` +
643
+ `L${right - radius},${top} `;
644
+
645
+ const pathRight = direction == BMPopoverIndicatorDirection.Right ?
646
+ // Draw the rounded corner, then the indicator and move to the end of the edge
647
+ `Q${right},${top} ${right},${top + radius} ` +
648
+ `L${right},${(position - knobWidth / 2 + top)} l${knobHeight},${(knobWidth / 2)} l${-knobHeight},${(knobWidth / 2)} ` +
649
+ `L${right},${bottom - radius} ` :
650
+ // Draw the rounded corner, then move to the end of the dge
651
+ `Q${right},${top} ${right},${top + radius} ` +
652
+ `L${right},${bottom - radius} `;
653
+
654
+ const pathBottom = direction === BMPopoverIndicatorDirection.Bottom ?
655
+ // Draw the rounded corner, then the indicator and move to the end of the edge
656
+ `Q${right},${bottom} ${right - radius},${bottom} ` +
657
+ `L${(position + knobWidth / 2 + left)},${bottom} l${(-knobWidth / 2)},${knobHeight} l${(-knobWidth / 2)},${-knobHeight} ` +
658
+ `L${left + radius},${bottom} ` :
659
+ // Draw the rounded corner, then move to the end of the edge
660
+ `Q${right},${bottom} ${right - radius},${bottom} ` +
661
+ `L${left + radius},${bottom} `;
662
+
663
+ const pathLeft = direction === BMPopoverIndicatorDirection.Left ?
664
+ // Draw the rounded corner coming from the bottom left, then the indicator
665
+ // afterwards move to the end of the edge and finally draw the rounded corner coming from the left to top
666
+ `Q${left},${bottom} ${left},${bottom - radius} ` +
667
+ `L${left},${(position + knobWidth / 2 + top)} l${-knobHeight},${(-knobWidth / 2)} l${knobHeight},${(-knobWidth / 2)} ` +
668
+ `L${left},${top + radius} ` +
669
+ `Q${left},${top} ${left + radius},${top} Z` :
670
+ // Draw the rounded corner coming from the bottom left, then move to
671
+ // the end of the edge and finally draw the rounded corner coming from the left to top
672
+ `Q${left},${bottom} ${left},${bottom - radius} ` +
673
+ `L${left},${top + radius} ` +
674
+ `Q${left},${top} ${left + radius},${top} Z`;
417
675
 
418
676
 
419
677
  let path = pathTop;
420
- path += `Q${frame.size.width + left},${top} ${frame.size.width + left},${top + radius} L${frame.size.width + left},${bottom - radius} `;
678
+ path += pathRight; //`Q${right},${top} ${right},${top + radius} L${right},${bottom - radius} `;
421
679
  path += pathBottom;
422
- path += `Q${left},${bottom} ${left},${bottom - radius} L${left},${top + radius} Q${left},${top} ${radius},${top} Z`;
680
+ path += pathLeft; //`Q${left},${bottom} ${left},${bottom - radius} L${left},${top + radius} Q${left},${top} ${radius},${top} Z`;
423
681
 
424
682
  return path;
425
683
  },