bm-core-ui 2.9.3 → 2.10.0

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.
Files changed (59) hide show
  1. package/build/@types/index.d.ts +353 -61
  2. package/build/BMCollectionView/BMCollectionView.js +77 -39
  3. package/build/BMCollectionView/BMCollectionViewFlowLayout.js +18 -5
  4. package/build/BMCoreUI.css +45 -18
  5. package/build/BMView/BMMenu.js +134 -7
  6. package/build/BMView/BMView_v2.5.js +62 -3
  7. package/build/BMWindow/BMConfirmationPopup.js +1 -1
  8. package/build/BMWindow/BMKeyboardShortcut.js +338 -88
  9. package/build/BMWindow/BMPopover/BMPopover.js +53 -12
  10. package/build/BMWindow/BMWindow.js +32 -6
  11. package/build/Core/BMCoreUI.js +20 -8
  12. package/build/Core/BMRect.js +1 -1
  13. package/build/images/AlignBottom.png +0 -0
  14. package/build/images/AlignCenterX.png +0 -0
  15. package/build/images/AlignCenterY.png +0 -0
  16. package/build/images/AlignLeading.png +0 -0
  17. package/build/images/AlignTop.png +0 -0
  18. package/build/images/AlignTrailing.png +0 -0
  19. package/build/images/AllConstraints.png +0 -0
  20. package/build/images/BottomConstraint.png +0 -0
  21. package/build/images/CenterXConstraint.png +0 -0
  22. package/build/images/CenterYConstraint.png +0 -0
  23. package/build/images/CoreUI2.png +0 -0
  24. package/build/images/CoreUI2@2x.png +0 -0
  25. package/build/images/Desktop.png +0 -0
  26. package/build/images/DesktopMini.png +0 -0
  27. package/build/images/EqualHeight.png +0 -0
  28. package/build/images/EqualHorizontalSpacing.png +0 -0
  29. package/build/images/EqualHorizontalSpacingInSuperview.png +0 -0
  30. package/build/images/EqualVerticalSpacing.png +0 -0
  31. package/build/images/EqualVerticalSpacingInSuperview.png +0 -0
  32. package/build/images/EqualWidth.png +0 -0
  33. package/build/images/HeightConstraint.png +0 -0
  34. package/build/images/InactiveConstraints.png +0 -0
  35. package/build/images/Layout.png +0 -0
  36. package/build/images/LayoutVariables.png +0 -0
  37. package/build/images/LeftConstraint.png +0 -0
  38. package/build/images/OwnConstraints.png +0 -0
  39. package/build/images/Phone.png +0 -0
  40. package/build/images/PhoneLandscape.png +0 -0
  41. package/build/images/PhoneLandscapeMini.png +0 -0
  42. package/build/images/PhoneMini.png +0 -0
  43. package/build/images/PhonePortrait.png +0 -0
  44. package/build/images/PhonePortraitMini.png +0 -0
  45. package/build/images/Properties.png +0 -0
  46. package/build/images/RightConstraint.png +0 -0
  47. package/build/images/SubviewConstraints.png +0 -0
  48. package/build/images/Tablet.png +0 -0
  49. package/build/images/TabletLandscape.png +0 -0
  50. package/build/images/TabletLandscapeMini.png +0 -0
  51. package/build/images/TabletMini.png +0 -0
  52. package/build/images/TabletPortrait.png +0 -0
  53. package/build/images/TabletPortraitMini.png +0 -0
  54. package/build/images/TopConstraint.png +0 -0
  55. package/build/images/WidthConstraint.png +0 -0
  56. package/build/index.js +2 -0
  57. package/lib/@types/BMCoreUI.min.d.ts +353 -61
  58. package/lib/BMCoreUI.min.js +3 -1
  59. package/package.json +9 -10
@@ -350,8 +350,17 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
350
350
  * The layout object managing this collection view's layout.
351
351
  */
352
352
  _layout: undefined, // <BMCollectionViewLayout>
353
- get layout() { return this._layout; },
353
+ get layout() {
354
+ if (this._transitionLayout && this._layout == this._transitionLayout) {
355
+ return this._layout.targetLayout;
356
+ }
357
+ else {
358
+ return this._layout;
359
+ }
360
+ },
354
361
  set layout(layout) {
362
+ // If data is being updated, wait for the update to finish before updating the
363
+ // layout
355
364
  if (this.isUpdatingData) {
356
365
  var self = this;
357
366
  this.registerDataCompletionCallback(function () {
@@ -360,6 +369,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
360
369
  return;
361
370
  }
362
371
 
372
+ // If an animation context is active, perform this change with an animation
363
373
  if (BMAnimationContextGetCurrent()) {
364
374
  if (!this.initialized) {
365
375
  this._layout = layout || new BMCollectionViewFlowLayout();
@@ -378,6 +388,12 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
378
388
  this.invalidateLayout();
379
389
  }
380
390
  },
391
+
392
+ /**
393
+ * Set to a transition layout while an animated layout update is in progress. Undefined
394
+ * in all other cases.
395
+ */
396
+ _transitionLayout: undefined, // <BMCollectionViewLayout, nullable>
381
397
 
382
398
  // MARK: Managed attributes
383
399
 
@@ -388,7 +404,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
388
404
  get frame() { return this._frame; },
389
405
  set frame(frame) {
390
406
  // If this frame is assigned as part of a layout animation, don't perform any changes
391
- if (this._layoutAnimator) return Object.getOwnPropertyDescriptor(BMView.prototype, 'frame').set.call(this, frame);
407
+ if (this._layoutAnimator) {
408
+ Object.getOwnPropertyDescriptor(BMView.prototype, 'frame').set.call(this, frame);
409
+ return;
410
+ }
392
411
 
393
412
  const currentFrame = this._frame && this._frame.copy();
394
413
 
@@ -747,8 +766,47 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
747
766
  this._supplementaryViewClasses[args.forReuseIdentifier] = supplementaryViewClass;
748
767
  },
749
768
 
750
- // @override - BMView
751
- initWithDOMNode(node) {
769
+
770
+ /**
771
+ * Initializes this collection view with the specified DOM node and default properties.
772
+ * The collection view will use a `BMCollectionViewFlowLayout` layout object.
773
+ * A valid object should be assigned to the `dataSet` property to fully initialize and use this collection view.
774
+ * @param node <DOMNode> The container that the collection view will manage. This should be an empty div element,
775
+ * otherwise the behaviour of the collection view will be undefined.
776
+ * {
777
+ * @param customScroll <Boolean, nullable> Defaults to `NO`. When set to `YES`, collection view will use custom scrolling in
778
+ * place of regular scrolling. Otherwise, it will use native scrolling. This should only be
779
+ * used with layouts where cell positions depend on the scroll position of collection view.
780
+ * This property cannot be changed after being set in the intializer.
781
+ * }
782
+ * @return <BMCollectionView> A collection view.
783
+ */
784
+ initWithDOMNode(node, args) {
785
+ var customScrollRequired = args && args.customScroll;
786
+
787
+ _BMCollectionViews.set(this, true);
788
+
789
+ this._container = BMJQueryShim.shimWithDOMNode(node);
790
+ this.layout = new BMCollectionViewFlowLayout();
791
+ this.cellCache = {};
792
+ this.retainedCells = [];
793
+ this.allCells = [];
794
+ this.supplementaryViewCache = {};
795
+
796
+ this.attributeCache = {};
797
+
798
+ this._selectedIndexPaths = [];
799
+
800
+ this.customScrollRequired = customScrollRequired;
801
+
802
+ this._cellClasses = {};
803
+ this._supplementaryViewClasses = {};
804
+
805
+ this._measures = {};
806
+ this._measuredIndexPaths = [];
807
+
808
+ this._cellLayoutQueue = BMViewLayoutQueue.layoutQueue();
809
+
752
810
  return BMView.prototype.initWithDOMNode.apply(this, arguments);
753
811
  },
754
812
 
@@ -787,7 +845,8 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
787
845
  },
788
846
 
789
847
  /**
790
- * Should not be invoked manually. It is invoked by collection view to create the initial layout and cells.
848
+ * Should not be invoked manually. Invoked by collection view the first time a data set is assigned
849
+ * to it to create the initial layout and cells.
791
850
  */
792
851
  _init: function () {
793
852
  // If currently invisible, delay the init
@@ -3417,6 +3476,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3417
3476
  if (i == event.changedTouches.length) return;
3418
3477
  }
3419
3478
 
3479
+ if (this.delegate && this.delegate.collectionViewWillFinishInteractiveMovementForCell) {
3480
+ this.delegate.collectionViewWillFinishInteractiveMovementForCell(this, cell, {atIndexPath: cell.indexPath});
3481
+ }
3482
+
3420
3483
  // Remove the previously added handlers from window
3421
3484
  window.removeEventListener(touchIdentifier !== undefined ? 'touchmove' : 'mousemove', mousemoveHandler, {capture: YES, passive: NO});
3422
3485
  window.removeEventListener(touchIdentifier !== undefined ? 'touchend' : 'mouseup', mouseupHandler, {capture: YES, passive: NO});
@@ -3440,10 +3503,6 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3440
3503
  let sourceRect = BMRectMakeWithNodeFrame(draggingShadow);
3441
3504
  let targetRect = BMRectMakeWithNodeFrame(cell.node);
3442
3505
 
3443
- if (this.delegate && this.delegate.collectionViewWillFinishInteractiveMovementForCell) {
3444
- this.delegate.collectionViewWillFinishInteractiveMovementForCell(this, cell, {atIndexPath: cell.indexPath});
3445
- }
3446
-
3447
3506
  // Resolve the current dragging operation
3448
3507
  resolveIsDragging();
3449
3508
 
@@ -4710,6 +4769,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
4710
4769
 
4711
4770
  // Temporarily make the transition layout the current layout
4712
4771
  this._layout = transitionLayout;
4772
+ this._transitionLayout = transitionLayout;
4713
4773
 
4714
4774
  // Retain all transitioning cells during this change
4715
4775
  let retainedCells = [];
@@ -4781,6 +4841,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
4781
4841
  });
4782
4842
 
4783
4843
  self._layout = layout;
4844
+ self._transitionLayout = undefined;
4784
4845
 
4785
4846
  if (self.iScroll) self.iScroll.refresh();
4786
4847
 
@@ -5026,8 +5087,13 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5026
5087
  this._updateSelectionIndexPaths();
5027
5088
 
5028
5089
  if (!animated) {
5090
+ this.layout.collectionViewWillStartUpdates(undefined);
5091
+
5029
5092
  // Instant data set changes are identical to layout invalidations.
5030
5093
  this.invalidateLayout();
5094
+
5095
+ // Inform the layout that the full data update has finished preparing and all animations were started
5096
+ self.layout.collectionViewDidStartUpdates();
5031
5097
 
5032
5098
  // Have the data set refresh the contents of each cell
5033
5099
  for (var i = 0; i < this.allCells.length; i++) {
@@ -6162,7 +6228,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
6162
6228
  * @return <BMCollectionView> A collection view.
6163
6229
  */
6164
6230
  BMCollectionView.collectionViewForNode = function (node, args) {
6165
- let collectionView = BMCollectionViewMakeWithContainer(BMJQueryShim.shimWithDOMNode(node), args);
6231
+ let collectionView = new this().initWithDOMNode(node, args);
6166
6232
 
6167
6233
  return collectionView;
6168
6234
  }
@@ -6193,35 +6259,7 @@ BMCollectionView.collectionView = function () {
6193
6259
  * @return <BMCollectionView> A collection view.
6194
6260
  */
6195
6261
  export function BMCollectionViewMakeWithContainer(container, options) {
6196
- var customScrollRequired = options && options.customScroll;
6197
- var collectionView = new BMCollectionView();
6198
-
6199
- _BMCollectionViews.set(collectionView, true);
6200
-
6201
- collectionView._container = container;
6202
- collectionView.layout = new BMCollectionViewFlowLayout();
6203
- collectionView.cellCache = {};
6204
- collectionView.retainedCells = [];
6205
- collectionView.allCells = [];
6206
- collectionView.supplementaryViewCache = {};
6207
-
6208
- collectionView.attributeCache = {};
6209
-
6210
- collectionView._selectedIndexPaths = [];
6211
-
6212
- collectionView.customScrollRequired = customScrollRequired;
6213
-
6214
- collectionView._cellClasses = {};
6215
- collectionView._supplementaryViewClasses = {};
6216
-
6217
- collectionView._measures = {};
6218
- collectionView._measuredIndexPaths = [];
6219
-
6220
- collectionView._cellLayoutQueue = BMViewLayoutQueue.layoutQueue();
6221
-
6222
- collectionView.initWithDOMNode(container[0]);
6223
-
6224
- return collectionView;
6262
+ return new BMCollectionView().initWithDOMNode(container[0], options);
6225
6263
  }
6226
6264
 
6227
6265
  // @endtype
@@ -22,20 +22,20 @@ export var BMCollectionViewTableLayoutSupplementaryView = Object.freeze({ // <en
22
22
  /**
23
23
  * Indicates that this supplementary view is a footer.
24
24
  */
25
- Footer: "Footer", // <enum>
25
+ Footer: "Footer", // <string>
26
26
 
27
27
  /**
28
28
  * Indicates that this supplementary view is a header.
29
29
  */
30
- Header: "Header", // <enum>
30
+ Header: "Header", // <string>
31
31
 
32
32
  /**
33
33
  * Indicates that this supplementary is an empty content view.
34
34
  */
35
- Empty: "Empty" // <enum>
35
+ Empty: "Empty" // <string>
36
36
  });
37
37
 
38
- // @type
38
+ // @endtype
39
39
 
40
40
  // @type BMCollectionViewTableLayout extends BMCollectionViewLayout
41
41
 
@@ -1394,7 +1394,7 @@ export var BMCollectionViewFlowLayoutOrientation = Object.freeze({ // <enum>
1394
1394
  * As the flow layout only provides supplementary views for section headers and footers and empty data sets just like the table layout,
1395
1395
  * the identifiers used by the table layout are reused in this case.
1396
1396
  */
1397
- export var BMCollectionViewFlowLayoutSupplementaryView = BMCollectionViewTableLayoutSupplementaryView;
1397
+ export var BMCollectionViewFlowLayoutSupplementaryView = BMCollectionViewTableLayoutSupplementaryView; // <typeof BMCollectionViewTableLayoutSupplementaryView>
1398
1398
 
1399
1399
  /**
1400
1400
  * The flow layout arranges cells in a horiztonally (TO DO) or vertically scrolling container.
@@ -1758,6 +1758,18 @@ BMCollectionViewFlowLayout.prototype = BMExtend(Object.create(BMCollectionViewLa
1758
1758
  // When using custom cell sizes, the layout will pre-compute the entire layout attributes
1759
1759
  this.cachedLayout = { sections: [] };
1760
1760
 
1761
+ // If this is invoked as part of a batch or animated layout update, ensure that the new layout
1762
+ // is measured up to at least the current collection view bounds
1763
+ if (this._expectedCellSize && this._copy) {
1764
+ target = target || {};
1765
+ if (target.targetRect) {
1766
+ target.targetRect = target.targetRect.rectByUnionWithRect(this.collectionView.bounds)
1767
+ }
1768
+ else {
1769
+ target.targetRect = this.collectionView.bounds;
1770
+ }
1771
+ }
1772
+
1761
1773
  /*
1762
1774
  declare interface BMCollectionViewFlowLayoutCache {
1763
1775
  availableWidth: number;
@@ -4885,6 +4897,7 @@ BMCollectionViewFlowLayout.prototype = BMExtend(Object.create(BMCollectionViewLa
4885
4897
  var copy = new BMCollectionViewFlowLayout();
4886
4898
 
4887
4899
  copy.cellSize = this.cellSize && this.cellSize.copy();
4900
+ copy.expectedCellSize = this.expectedCellSize && this.expectedCellSize.copy();
4888
4901
  copy.rowSpacing = this.rowSpacing;
4889
4902
 
4890
4903
  copy.showsHeaders = this.showsHeaders;
@@ -256,24 +256,24 @@
256
256
  }
257
257
 
258
258
  @media (prefers-color-scheme: dark) {
259
- .BMWindow {
259
+ .BMDarkModeAuto.BMWindow {
260
260
  box-shadow: rgba(0, 0, 0, .66) 0px 0px 0px 1px, rgba(0, 0, 0, .66) 0px 12px 64px;
261
261
  background-color: rgba(255, 255, 255, .6);
262
262
  }
263
263
 
264
- .BMWindowInactive {
264
+ .BMDarkModeAuto.BMWindowInactive {
265
265
  background: rgba(70, 70, 70, 1);
266
266
  }
267
267
 
268
- .BMNonModalWindow {
268
+ .BMDarkModeAuto.BMNonModalWindow {
269
269
  box-shadow: rgba(0, 0, 0, .66) 0px 0px 0px 1px, rgba(0, 0, 0, .66) 0px 6px 32px;
270
270
  }
271
271
 
272
- .BMWindow.BMKeyWindow, .BMToolWindow {
272
+ .BMDarkModeAuto.BMWindow.BMKeyWindow, .BMDarkModeAuto.BMToolWindow {
273
273
  box-shadow: rgba(0, 0, 0, .66) 0px 0px 0px 1px, rgba(0, 0, 0, .66) 0px 12px 64px;
274
274
  }
275
275
 
276
- .BMWindow::after {
276
+ .BMDarkModeAuto.BMWindow::after {
277
277
  content: '';
278
278
  position: absolute;
279
279
  pointer-events: none;
@@ -288,7 +288,7 @@
288
288
  box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, .25) inset;
289
289
  }
290
290
 
291
- .BMWindowDragHandle {
291
+ .BMDarkModeAuto.BMWindowDragHandle {
292
292
  color: rgba(255, 255, 255, .33);
293
293
  }
294
294
  }
@@ -313,7 +313,7 @@
313
313
  /* In theory, this would break browsers that support dark mode but not backdrop filter;
314
314
  In practice, only Safari supports both. */
315
315
  @media (prefers-color-scheme: dark) {
316
- .BMWindowContent {
316
+ .BMDarkModeAuto.BMWindowContent {
317
317
  background-color: rgba(0, 0, 0, .75) !important;
318
318
  }
319
319
  }
@@ -323,7 +323,7 @@
323
323
  }
324
324
 
325
325
  @media (prefers-color-scheme: dark) {
326
- .BMWindowBackground {
326
+ .BMDarkModeAuto.BMWindowBackground {
327
327
  background-color: #1e1e1e !important; /* tw-important */
328
328
  }
329
329
  }
@@ -1294,6 +1294,10 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1294
1294
  -webkit-tap-highlight-color: transparent;
1295
1295
  }
1296
1296
 
1297
+ .BMMenuItemDisabled.BMMenuItem {
1298
+ color: rgba(0, 0, 0, .4);
1299
+ }
1300
+
1297
1301
  .BMLayoutEditorConstraintPopupOption {
1298
1302
  padding-right: 64px;
1299
1303
  }
@@ -1330,11 +1334,21 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1330
1334
  flex-grow: 1;
1331
1335
  }
1332
1336
 
1337
+ .BMMenuItemHint {
1338
+ margin-left: 48px;
1339
+
1340
+ display: flex;
1341
+ align-items: center;
1342
+ justify-content: center;
1343
+
1344
+ padding: 0px 16px;
1345
+
1346
+ opacity: .33;
1347
+ }
1348
+
1333
1349
  .BMMenuItemDisclosure {
1334
1350
  width: 24px;
1335
1351
  height: 24px;
1336
-
1337
- margin-left: 48px;
1338
1352
 
1339
1353
  font-weight: 600;
1340
1354
 
@@ -1343,6 +1357,12 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1343
1357
  display: flex;
1344
1358
  align-items: center;
1345
1359
  justify-content: center;
1360
+
1361
+ display: none;
1362
+ }
1363
+
1364
+ .BMMenuItemDisclosure.BMMenuItemDisclosureSubmenu {
1365
+ display: block;
1346
1366
  }
1347
1367
 
1348
1368
  .BMMenuItemDisclosureSubmenu::after {
@@ -1371,7 +1391,7 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1371
1391
  }
1372
1392
  }
1373
1393
 
1374
- .BMLayoutEditorConstraintPopupOption:hover, .BMMenuTouch > .BMMenuItem:active, .BMMenuItem.BMMenuItemHighlighted {
1394
+ .BMLayoutEditorConstraintPopupOption:hover, .BMMenuTouch > .BMMenuItem:not(.BMMenuItemDisabled):active, .BMMenuItem.BMMenuItemHighlighted {
1375
1395
  background: rgb(0, 128, 255);
1376
1396
  color: white;
1377
1397
  }
@@ -1381,7 +1401,7 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1381
1401
  color: rgba(0, 0, 0, .8);
1382
1402
  }
1383
1403
 
1384
- .BMLayoutEditorConstraintPopupOption:active, .BMMenuTouch > .BMMenuItem:hover {
1404
+ .BMLayoutEditorConstraintPopupOption:active, .BMMenuTouch > .BMMenuItem:not(.BMMenuItemDisabled):hover {
1385
1405
  background: transparent;
1386
1406
  color: rgba(0, 0, 0, .8);
1387
1407
  }
@@ -1410,7 +1430,11 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
1410
1430
  color: rgba(255, 255, 255, .9);
1411
1431
  }
1412
1432
 
1413
- .BMLayoutEditorConstraintPopupOption:active, .BMMenuTouch > .BMMenuItem:hover {
1433
+ .BMMenuItem.BMMenuItemDisabled {
1434
+ color: rgba(255, 255, 255, .45);
1435
+ }
1436
+
1437
+ .BMLayoutEditorConstraintPopupOption:active, .BMMenuTouch > .BMMenuItem:not(.BMMenuItemDisabled):hover {
1414
1438
  background: transparent;
1415
1439
  color: white;
1416
1440
  }
@@ -2528,6 +2552,9 @@ input:not(:checked) + .BMWindowSwitchGutter > .BMWindowSwitchKnob, .BMWindowSwit
2528
2552
 
2529
2553
  .BMPopover {
2530
2554
  position: absolute;
2555
+
2556
+ /* This is required otherwise the mix-blend-mode on the drop shadow layer doesn't work */
2557
+ z-index: unset !important;
2531
2558
  }
2532
2559
 
2533
2560
  .BMPopoverOverlay {
@@ -2605,7 +2632,7 @@ The drop shadow layer contains the drop shadow effect only.
2605
2632
  }
2606
2633
 
2607
2634
  @media (prefers-color-scheme: dark) {
2608
- .BMPopoverDropShadowContainer {
2635
+ .BMDarkModeAuto.BMPopoverDropShadowContainer {
2609
2636
  filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 1)) drop-shadow(0px 12px 64px rgba(0, 0, 0, .66));
2610
2637
  }
2611
2638
  }
@@ -2642,11 +2669,11 @@ The background layer contains the background.
2642
2669
  }
2643
2670
 
2644
2671
  @media (prefers-color-scheme: dark) {
2645
- .BMPopoverBackground {
2672
+ .BMDarkModeAuto.BMPopoverBackground {
2646
2673
  background: transparent;
2647
2674
  }
2648
2675
 
2649
- .BMPopoverBackgroundDarkModeContainer {
2676
+ .BMDarkModeAuto.BMPopoverBackgroundDarkModeContainer {
2650
2677
  display: block;
2651
2678
 
2652
2679
  position: absolute;
@@ -2659,7 +2686,7 @@ The background layer contains the background.
2659
2686
  opacity: .8;
2660
2687
  }
2661
2688
 
2662
- .BMPopoverBackgroundDarkModeOutline {
2689
+ .BMDarkModeAuto.BMPopoverBackgroundDarkModeOutline {
2663
2690
  background-color: rgb(100, 100, 100);
2664
2691
 
2665
2692
  position: absolute;
@@ -2670,7 +2697,7 @@ The background layer contains the background.
2670
2697
  top: 0px;
2671
2698
  }
2672
2699
 
2673
- .BMPopoverBackgroundDarkModeFill {
2700
+ .BMDarkModeAuto.BMPopoverBackgroundDarkModeFill {
2674
2701
  background-color: rgb(40, 40, 40);
2675
2702
 
2676
2703
  position: absolute;
@@ -128,12 +128,72 @@ BMMenuItem.prototype = {
128
128
  this._CSSClass = cls || '';
129
129
  },
130
130
 
131
+ /**
132
+ * An optional key sequence that may be used to select this menu item via a keyboard shortcut.
133
+ * Updates to this property do not take effect while a menu displaying this menu item is open.
134
+ */
135
+ _keySequence: undefined, // <BMKeySequence, nullable>
136
+ get keySequence() {
137
+ return this._keySequence;
138
+ },
139
+ set keySequence(sequence) {
140
+ this._keySequence = sequence;
141
+ },
142
+
143
+ /**
144
+ * An optional hint HTML text that will be displayed next to the menu item.
145
+ */
146
+ _hintHTML: undefined, // <String, nullable>
147
+ get hintHTML() {
148
+ return this._hintHTML;
149
+ },
150
+ set hintHTML(hint) {
151
+ this._hintHTML = hint;
152
+ if (this._node) {
153
+ const hintContainer = this._node.querySelector('.BMMenuItemHint');
154
+ if (hintContainer) {
155
+ hintContainer.innerHTML = hint;
156
+ }
157
+ }
158
+ },
159
+
131
160
  /**
132
161
  * The node that represents this menu item, available while its menu
133
162
  * is open.
134
163
  */
135
164
  _node: undefined, // <DOMNode, nullable>
136
165
 
166
+ /**
167
+ * Defaults to `YES`. Controls whether this menu item is enabled. If this menu
168
+ * item is not enabled, it can not be highlighted or selected.
169
+ */
170
+ _enabled: YES, // <Boolean>
171
+ get enabled() {
172
+ return this._enabled;
173
+ },
174
+ set enabled(enabled) {
175
+ if (this._enabled == enabled) {
176
+ return;
177
+ }
178
+
179
+ // Separators can never be enabled
180
+ if (this._isSeparator) {
181
+ return;
182
+ }
183
+
184
+ this._enabled = enabled;
185
+
186
+ if (this._node) {
187
+ /** @type {HTMLElement} */ const node = this._node;
188
+ if (enabled) {
189
+ node.classList.remove('BMMenuItemDisabled');
190
+ }
191
+ else {
192
+ node.classList.add('BMMenuItemDisabled');
193
+ }
194
+ }
195
+ },
196
+
137
197
  /**
138
198
  * Constructs and returns a menu item with the given name.
139
199
  * Optionally, the menu item may have an icon associated with it as well as an
@@ -145,12 +205,13 @@ BMMenuItem.prototype = {
145
205
  *
146
206
  * @param action <void ^ (BMMenuItem), nullable> If specified, this is a function that will be invoked when this menu item is selected.
147
207
  * This function will receive the selected menu item as a parameter.
208
+ * @param enabled <Boolean, nullable> Defaults to `YES`. Controls whether this item is enabled and can be selected.
148
209
  * @param submenu <BMMenu, nullable> Optional. If specified, this represents a menu that opens from this menu item.
149
210
  * @param userInfo <AnyObject, nullable> Optional. If specified, this represents additional arbitrary data attached to this menu item.
150
211
  * }
151
212
  * @return <BMMenuItem> This menu item.
152
213
  */
153
- initWithName(name, {icon, action, submenu, userInfo} = {}) {
214
+ initWithName(name, {icon, action, submenu, enabled = true, userInfo} = {}) {
154
215
  this._name = name;
155
216
  this._icon = icon;
156
217
  this._action = action;
@@ -159,8 +220,11 @@ BMMenuItem.prototype = {
159
220
 
160
221
  if (name.startsWith('---')) {
161
222
  this._isSeparator = YES;
223
+ this._enabled = NO;
162
224
  }
163
225
 
226
+ this._enabled = enabled;
227
+
164
228
  return this;
165
229
  }
166
230
 
@@ -177,6 +241,7 @@ BMMenuItem.prototype = {
177
241
  *
178
242
  * @param action <void ^ (BMMenuItem), nullable> If specified, this is a function that will be invoked when this menu item is selected.
179
243
  * This function will receive the selected menu item as a parameter.
244
+ * @param enabled <Boolean, nullable> Defaults to `YES`. Controls whether this item is enabled and can be selected.
180
245
  * @param submenu <BMMenu, nullable> Optional. If specified, this represents a menu that opens from this menu item.
181
246
  * @param userInfo <AnyObject, nullable> Optional. If specified, this represents additional arbitrary data attached to this menu item.
182
247
  * }
@@ -225,7 +290,7 @@ BMMenu.prototype = {
225
290
  },
226
291
 
227
292
  /**
228
- * Defaults to 24. The size to use for this menu's icons.
293
+ * Defaults to `24`. The size to use for this menu's icons.
229
294
  */
230
295
  _iconSize: 24, // <Number>
231
296
  get iconSize() {
@@ -461,6 +526,15 @@ BMMenu.prototype = {
461
526
  escape.preventsDefault = YES;
462
527
  BMView.registerKeyboardShortcut(escape, {forNode: menuNode});
463
528
 
529
+ // Register a keyboard shortcut for each menu item that specifies one
530
+ for (const item of this._items) {
531
+ if (item.keySequence) {
532
+ const shortcut = BMKeyboardShortcut.keyboardShortcutWithKeySequence(item.keySequence, {target: this, action: '_selectMenuItemWithEvent'});
533
+ shortcut._menuItem = item;
534
+ BMView.registerKeyboardShortcut(shortcut, {forNode: menuNode});
535
+ }
536
+ }
537
+
464
538
  // The overlay which intercepts clicks outside of the menu
465
539
  const menuContainer = container || document.createElement('div');
466
540
  if (!container) {
@@ -560,6 +634,12 @@ BMMenu.prototype = {
560
634
  itemText.innerText = item.name;
561
635
  itemNode.appendChild(itemText);
562
636
 
637
+ // Create the hint container
638
+ const hint = document.createElement('div');
639
+ hint.classList.add('BMMenuItemHint');
640
+ hint.innerHTML = item._hintHTML || '';
641
+ itemNode.appendChild(hint);
642
+
563
643
  // Create the disclosure container
564
644
  const submenuIcon = document.createElement('div');
565
645
  submenuIcon.classList.add('BMMenuItemDisclosure');
@@ -568,9 +648,18 @@ BMMenu.prototype = {
568
648
  submenuIcon.classList.add('BMMenuItemDisclosureSubmenu');
569
649
  }
570
650
  itemNode.appendChild(submenuIcon);
651
+
652
+ if (!item._enabled) {
653
+ itemNode.classList.add('BMMenuItemDisabled');
654
+ }
571
655
 
572
656
  // TWhen clicking an item, trigger its action, if it has one
573
657
  itemNode.addEventListener('click', event => {
658
+ // Disabled items can't be clicked
659
+ if (!item._enabled) {
660
+ return;
661
+ }
662
+
574
663
  if (item.submenu && this._sourceNodeShadow) {
575
664
  // For touch menus, if the item has a submenu, open it
576
665
  this._openSubmenuForMenuItem(item, {animated: YES, delayEvents: NO, acquireFocus: YES});
@@ -611,6 +700,12 @@ BMMenu.prototype = {
611
700
  // When moving the mouse over an item, set the highlight index to it,
612
701
  // allowing further keyboard navigation to continute from this item
613
702
  itemNode.addEventListener('mouseover', event => {
703
+ // Disabled items can't be highlighted and can't open submenus
704
+ if (!item._enabled) {
705
+ this.highlightedIndex = -1;
706
+ return;
707
+ }
708
+
614
709
  if (this._highlightedIndex == index) return;
615
710
 
616
711
  // If events should be delayed, don't process this mouseover event
@@ -642,7 +737,8 @@ BMMenu.prototype = {
642
737
  _kind: BMMenuKind.Menu,
643
738
 
644
739
  /**
645
- * Animatable. Shows this menu from the given DOM node.
740
+ * Animatable. Shows this menu from the specified DOM node. The menu will be displayed as a touch menu,
741
+ * highlighting the source node and de-emphasizing the rest of the document.
646
742
  * @param node <DOMNode> The node from which to show this menu.
647
743
  * {
648
744
  * @param animated <Boolean, nullable> Defaults to `NO`. If set to `YES`, this change will be animated.
@@ -1315,8 +1411,8 @@ BMMenu.prototype = {
1315
1411
  while (YES) {
1316
1412
  previousIndex = BMNumberByConstrainingNumberToBounds(previousIndex - 1, 0, this._items.length - 1);
1317
1413
 
1318
- // If item at the previous index isn't a separator, highlight it
1319
- if (!this._items[previousIndex]._isSeparator) break;
1414
+ // If item at the previous index isn't a separator or disabled, highlight it
1415
+ if (!this._items[previousIndex]._isSeparator && this._items[previousIndex]._enabled) break;
1320
1416
 
1321
1417
  // If the start of the items array has been reached and a viable highlightable item hasn't
1322
1418
  // been found, don't take any action
@@ -1337,8 +1433,8 @@ BMMenu.prototype = {
1337
1433
  while (YES) {
1338
1434
  nextIndex = BMNumberByConstrainingNumberToBounds(nextIndex + 1, 0, this._items.length - 1);
1339
1435
 
1340
- // If item at the next index isn't a separator, highlight it
1341
- if (!this._items[nextIndex]._isSeparator) break;
1436
+ // If item at the next index isn't a separator or disabled, highlight it
1437
+ if (!this._items[nextIndex]._isSeparator && this._items[nextIndex]._enabled) break;
1342
1438
 
1343
1439
  // If the end of the items array has been reached and a viable highlightable item hasn't
1344
1440
  // been found, don't take any action
@@ -1375,6 +1471,37 @@ BMMenu.prototype = {
1375
1471
  }
1376
1472
  },
1377
1473
 
1474
+ /**
1475
+ * Invoked when the key sequence for a menu item is pressed.
1476
+ * @param event <KeyboardEvent> The event that triggered this action.
1477
+ * {
1478
+ * @param forKeyboardShortcut <BMKeyboardShortcut> The keyboard shortcut that was triggered.
1479
+ * }
1480
+ */
1481
+ _selectMenuItemWithEvent(event, {forKeyboardShortcut: shortcut}) {
1482
+ const item = shortcut._menuItem;
1483
+
1484
+ // The item must not be a separator or disabled
1485
+ if (item._isSeparator || !item._enabled) {
1486
+ return;
1487
+ }
1488
+
1489
+ if (!this._node) {
1490
+ return;
1491
+ }
1492
+
1493
+ this._node.inert = true;
1494
+ this._node.style.pointerEvents = 'none';
1495
+
1496
+ // Otherwise highlight the item then send a return key to select it
1497
+ const index = this._items.indexOf(item);
1498
+ this.highlightedIndex = index;
1499
+
1500
+ setTimeout(() => {
1501
+ this.returnPressedWithEvent(event);
1502
+ }, 50);
1503
+ },
1504
+
1378
1505
  /**
1379
1506
  * Invoked when the return or spacebar key is pressed.
1380
1507
  * @param event <KeyboardEvent> The event that triggered this action.