bm-core-ui 2.7.0 → 2.8.3

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.
@@ -15,6 +15,7 @@ import {_BMCollectionViewTransitionLayout} from './BMCollectionViewLayout'
15
15
  import {IScroll} from '../iScroll/iscroll-probe'
16
16
  import { BMLayoutAttribute } from '../BMView/BMLayoutConstraint_v2.5'
17
17
  import { BMMenuSourceNodeCSSClass } from '../BMView/BMMenu'
18
+ import { BMKeyboardShortcut, BMKeyboardShortcutModifier } from '../BMWindow/BMKeyboardShortcut'
18
19
 
19
20
  // When set to YES, this will cause collection view to use static animation contexts when setting up animations
20
21
  const BM_COLLECTION_VIEW_USE_STATIC_CONTEXT = YES;
@@ -515,6 +516,13 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
515
516
  */
516
517
  _bounds: undefined, // <BMRect>
517
518
  get bounds() { return this._bounds; },
519
+
520
+ /**
521
+ * A rect that represents the part of the bounds that are visible on screen.
522
+ */
523
+ get visibleBounds() { // <BMRect>
524
+ return new BMRect(BMPointMake(this._bounds.origin.x + this._offscreenBufferSize, this._bounds.origin.y + this._offscreenBufferSize), this._frame.size.copy());
525
+ },
518
526
 
519
527
  /**
520
528
  * The number of pixels by which to extend the bounds to preload layout attributes and render off-screen elements.
@@ -682,6 +690,30 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
682
690
  */
683
691
  assignsReuseIdentifierAsClass: YES, // <Boolean>
684
692
 
693
+ /**
694
+ * Defaults to `YES`. When set to `NO`, keyboard navigation is disabled.
695
+ */
696
+ _supportsKeyboardNavigation: NO, // <Boolean>
697
+
698
+ get supportsKeyboardNavigation() {
699
+ return this._supportsKeyboardNavigation;
700
+ },
701
+
702
+ set supportsKeyboardNavigation(supports) {
703
+ if (supports != this._supportsKeyboardNavigation) {
704
+ this._supportsKeyboardNavigation = supports;
705
+
706
+ if (this.initialized) {
707
+ if (supports) {
708
+ this.configureKeyboardShortcuts();
709
+ }
710
+ else {
711
+ this.disableKeyboardShortcuts();
712
+ }
713
+ }
714
+ }
715
+ },
716
+
685
717
  /**
686
718
  * Registers a class to be used when creating cells with the given reuse identifier.
687
719
  * This does not affect already existing cells, but newly created cells with the given identifier will be created
@@ -962,6 +994,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
962
994
  delay: 0
963
995
  }, delegateOptions));
964
996
  }
997
+
998
+ if (this._supportsKeyboardNavigation) {
999
+ this.configureKeyboardShortcuts();
1000
+ }
965
1001
 
966
1002
  this.initialized = YES;
967
1003
 
@@ -2223,8 +2259,8 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2223
2259
  else {
2224
2260
  // Otherwise use the cell's own CSS size for this purpose
2225
2261
  size = BMSizeMake(
2226
- cell._variables[BMLayoutAttribute.Width].value() | cell.node.offsetWidth,
2227
- cell._variables[BMLayoutAttribute.Height].value() | cell.node.offsetHeight
2262
+ cell._variables[BMLayoutAttribute.Width].value() || cell.node.offsetWidth,
2263
+ cell._variables[BMLayoutAttribute.Height].value() || cell.node.offsetHeight
2228
2264
  );
2229
2265
  }
2230
2266
 
@@ -2376,7 +2412,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2376
2412
  node.style.cssText = "position: absolute; left: 0px; top: 0px; visibility: hidden; overflow: visible;";
2377
2413
 
2378
2414
  var cell = Object.create(cellClass.prototype).initWithCollectionView(this, {reuseIdentifier: identifier, node: node, kind: BMCollectionViewLayoutAttributesType.Cell});
2379
- cell._layoutQueue = this._cellLayoutQueue;
2415
+ cell.layoutQueue = this._cellLayoutQueue;
2380
2416
  //BMCollectionViewCellMakeForCollectionView(this, {withReuseIdentifier: identifier});
2381
2417
 
2382
2418
 
@@ -2413,6 +2449,28 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2413
2449
  var useCapture = NO;
2414
2450
 
2415
2451
  var canDrag = NO;
2452
+
2453
+ // Stops processing the current event sequence
2454
+ const cancelProcessing = () => {
2455
+ cellEventIsMouseDown = NO;
2456
+ cellEventCanTouchDrag = NO;
2457
+ cellEventTrackedTouch = undefined;
2458
+
2459
+ window.removeEventListener('mousemove', cell.mousemoveHandler, YES);
2460
+ window.removeEventListener('mouseup', cell.mouseupHandler, YES);
2461
+
2462
+ cellEventClicks = 0;
2463
+
2464
+ if (cellEventLongClickTimeout) {
2465
+ window.clearTimeout(cellEventLongClickTimeout);
2466
+ cellEventLongClickTimeout = undefined;
2467
+ }
2468
+
2469
+ if (cellEventDoubleClickTimeout) {
2470
+ window.clearTimeout(cellEventDoubleClickTimeout);
2471
+ cellEventDoubleClickTimeout = undefined;
2472
+ }
2473
+ }
2416
2474
 
2417
2475
  // Click, double click, long click, tap, double tap and long tap handlers
2418
2476
  cell.mousedownHandler = function (event) {
@@ -2428,6 +2486,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2428
2486
  event.target.parentNode.nodeName == 'BUTTON' || event.target.parentNode.nodeName == 'INPUT' || event.target.parentNode.nodeName == 'LABEL' ||
2429
2487
  event.target.classList.contains('BMCollectionViewCellEventHandler') || event.target.parentNode.classList.contains('BMCollectionViewCellEventHandler') ||
2430
2488
  event.target.classList.contains('widget-foldingpanel-header') ||
2489
+ event.target.hasAttribute('contenteditable') ||
2431
2490
  // This a specific Thingworx workaround that depends on jQuery
2432
2491
  (window.$ ? window.$(event.target).parents('.widget-dhxdropdown').length : false)) {
2433
2492
  return;
@@ -2472,6 +2531,14 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2472
2531
  // TODO: Consider if mouse events should also trigger drag & drop in this case
2473
2532
 
2474
2533
  if (canDrag) {
2534
+ // If a different collection view already handled this event, stop processing this sequence
2535
+ if (event.BM_Handled) {
2536
+ cancelProcessing();
2537
+ return;
2538
+ }
2539
+
2540
+ event.BM_Handled = YES;
2541
+
2475
2542
  // These events will be processed by the interactive movement handlers from this point on
2476
2543
  // So the global mousemove and mouseup handlers should be unregistered
2477
2544
  window.removeEventListener('mousemove', cell.mousemoveHandler, YES);
@@ -2524,6 +2591,12 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2524
2591
  // to fire even after the touch point has moved outside of the element
2525
2592
  // Because of this, mousemove is attached to window for the duration of a click and then released at the end
2526
2593
  cell.mousemoveHandler = function (event) {
2594
+ // If this event was already handled, stop processing
2595
+ if (event.BM_Handled) {
2596
+ cancelProcessing();
2597
+ return;
2598
+ }
2599
+
2527
2600
  if (_BMCoreUIIsDragging) return void (event.preventDefault(), event.stopPropagation());
2528
2601
  if (canDrag) event.preventDefault();
2529
2602
  event.originalEvent = event;
@@ -2576,6 +2649,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
2576
2649
  }*/
2577
2650
 
2578
2651
  if (canDrag) {
2652
+ // Mark this event as handled, so that when it bubbles up to other collection views they don't also trigger
2653
+ // drag & drop
2654
+ event.BM_Handled = YES;
2655
+
2579
2656
  if (self.delegate && self.delegate.collectionViewWillBeginInteractiveMovementForCell) {
2580
2657
  self.delegate.collectionViewWillBeginInteractiveMovementForCell(self, cell, {atIndexPath: cell.indexPath});
2581
2658
  }
@@ -3622,7 +3699,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3622
3699
  // Otherwise construct a new one
3623
3700
  var cellClass = this._supplementaryViewClasses[identifier] || this._cellClass;
3624
3701
  var cell = Object.create(cellClass.prototype).initWithCollectionView(this, {reuseIdentifier: identifier, node: node, kind: BMCollectionViewLayoutAttributesType.SupplementaryView});
3625
- cell._layoutQueue = this._cellLayoutQueue;
3702
+ cell.layoutQueue = this._cellLayoutQueue;
3626
3703
 
3627
3704
  // When using custom classes, it is no longer the data set object's responsibility to provide the contents for the cell
3628
3705
  if (cellClass == BMCollectionViewCell) cell.element.append(this._dataSet.contentsForSupplementaryViewWithIdentifier(identifier));
@@ -3910,6 +3987,11 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3910
3987
  cellWasClicked: function (cell, options) {
3911
3988
  if (options && options.withEvent) options.withEvent._BMOriginalTarget = cell.node;
3912
3989
 
3990
+ // Acquire focus if not already owned
3991
+ if (document.activeElement != this.node && this._supportsKeyboardNavigation) {
3992
+ this.node.focus();
3993
+ }
3994
+
3913
3995
  // Forward this event to the delegate, giving it a chance to handle the event
3914
3996
  var eventHandled = NO;
3915
3997
  if (this.delegate && this.delegate.collectionViewCellWasClicked) {
@@ -3918,15 +4000,26 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3918
4000
 
3919
4001
  // If the delegate did handle this event, do not invoke the default behavior
3920
4002
  if (eventHandled) return;
3921
-
3922
- // The default click behavior is to toggle cells' selection state
4003
+
4004
+ // The default behaviour when clicking cells is to highlight them
4005
+ if (this._highlightedIndexPath && !this._highlightedIndexPath.isLooselyEqualToIndexPath(cell.indexPath, {usingComparator: this.identityComparator})) {
4006
+ let canHighlight = YES;
4007
+ if (this.delegate && this.delegate.collectionViewCanHighlightCellAtIndexPath) {
4008
+ canHighlight = this.delegate.collectionViewCanHighlightCellAtIndexPath(this, cell.indexPath, {withEvent: options.withEvent});
4009
+ }
4010
+
4011
+ if (canHighlight) {
4012
+ this._highlightIndexPath(cell.indexPath, {withEvent: options.withEvent});
4013
+ }
4014
+ }
4015
+
4016
+ // The previous default click behavior is to toggle cells' selection state
3923
4017
  // Ask the delegate if the cell is selectable or deselectable,
3924
4018
  // depending on whether the cell is currently selected or not.
3925
4019
  var selectable = YES;
3926
- var selected;
3927
-
4020
+ const selected = this.isCellAtIndexPathSelected(cell.indexPath);
3928
4021
 
3929
- if (selected = this.isCellAtIndexPathSelected(cell.indexPath)) {
4022
+ if (selected) {
3930
4023
  if (this.delegate && typeof this.delegate.collectionViewCanDeselectCellAtIndexPath === 'function') {
3931
4024
  selectable = this.delegate.collectionViewCanDeselectCellAtIndexPath(this, cell.indexPath);
3932
4025
  }
@@ -3959,6 +4052,11 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3959
4052
  cellWasDoubleClicked: function (cell, options) {
3960
4053
  if (options && options.withEvent) options.withEvent._BMOriginalTarget = cell.node;
3961
4054
 
4055
+ // Acquire focus if not already owned
4056
+ if (document.activeElement != this.node && this._supportsKeyboardNavigation) {
4057
+ this.node.focus();
4058
+ }
4059
+
3962
4060
  // Forward this event to the delegate, giving it a chance to handle the event
3963
4061
  var eventHandled = NO;
3964
4062
  if (this.delegate && this.delegate.collectionViewCellWasDoubleClicked) {
@@ -3975,6 +4073,11 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
3975
4073
  */
3976
4074
  cellWasLongClicked: function (cell, options) {
3977
4075
  if (options && options.withEvent) options.withEvent._BMOriginalTarget = cell.node;
4076
+
4077
+ // Acquire focus if not already owned
4078
+ if (document.activeElement != this.node && this._supportsKeyboardNavigation) {
4079
+ this.node.focus();
4080
+ }
3978
4081
 
3979
4082
  // Forward this event to the delegate, giving it a chance to handle the event
3980
4083
  var eventHandled = NO;
@@ -4005,6 +4108,223 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
4005
4108
  return eventHandled;
4006
4109
  },
4007
4110
 
4111
+
4112
+ /************************************* HIGHLIGHT HANDLERS ********************************/
4113
+
4114
+ /**
4115
+ * An array that contains the registered keyboard navigation shortcuts.
4116
+ */
4117
+ _keyboardNavigationShortcuts: undefined, // <[BMKeyboardShortcut]>
4118
+
4119
+ /**
4120
+ * @protected
4121
+ * Configures the keyboard shortcuts used by the collection view for highlighting.
4122
+ * Subclasses overriding this may invoke the superclass implementation to retain the default keyboard
4123
+ * shortcuts.
4124
+ */
4125
+ configureKeyboardShortcuts() {
4126
+ // If the shortcuts have already been registered, do nothing
4127
+ if (this._keyboardNavigationShortcuts) return;
4128
+
4129
+ this._keyboardNavigationShortcuts = [];
4130
+
4131
+ // Register the arrow keys, which are used for highlighting
4132
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowLeft', {target: this, action: '_keyboardArrowPressedWithEvent'}));
4133
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowRight', {target: this, action: '_keyboardArrowPressedWithEvent'}));
4134
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowUp', {target: this, action: '_keyboardArrowPressedWithEvent'}));
4135
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowDown', {target: this, action: '_keyboardArrowPressedWithEvent'}));
4136
+
4137
+ // Register the shift + arrow keys, which are used for block selection
4138
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowLeft', {target: this, action: '_keyboardArrowPressedWithEvent', modifiers: [BMKeyboardShortcutModifier.Shift]}));
4139
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowRight', {target: this, action: '_keyboardArrowPressedWithEvent', modifiers: [BMKeyboardShortcutModifier.Shift]}));
4140
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowUp', {target: this, action: '_keyboardArrowPressedWithEvent', modifiers: [BMKeyboardShortcutModifier.Shift]}));
4141
+ this._keyboardNavigationShortcuts.push(BMKeyboardShortcut.keyboardShortcutWithKeyCode('ArrowDown', {target: this, action: '_keyboardArrowPressedWithEvent', modifiers: [BMKeyboardShortcutModifier.Shift]}));
4142
+
4143
+ this._keyboardNavigationShortcuts.forEach(k => this.registerKeyboardShortcut(k));
4144
+ },
4145
+
4146
+ /**
4147
+ * @protected
4148
+ * Removes the keyboard shortcuts used by the collection view for highlighting.
4149
+ * Subclasses overriding this should invoke the superclass implementation to remove the default keyboard
4150
+ * shortcuts.
4151
+ */
4152
+ disableKeyboardShortcuts() {
4153
+ // If the shortcuts have already been removed, do nothing
4154
+ if (!this._keyboardNavigationShortcuts) return;
4155
+
4156
+ this._keyboardNavigationShortcuts.forEach(k => this.unregisterKeyboardShortcut(k));
4157
+ this._keyboardNavigationShortcuts = undefined;
4158
+ },
4159
+
4160
+ /**
4161
+ * Invoked when an arrow key is pressed on the keyboard while this collection view has keyboard focus.
4162
+ * @param event <KeyboardEvent> The keyboard event that triggered this action.
4163
+ */
4164
+ _keyboardArrowPressedWithEvent(event) {
4165
+ switch (event.code) {
4166
+ case 'ArrowLeft':
4167
+ case 'ArrowUp':
4168
+ case 'ArrowRight':
4169
+ case 'ArrowDown':
4170
+ this.keyboardArrowPressed(event.code, {withEvent: event});
4171
+ break;
4172
+ }
4173
+ },
4174
+
4175
+ /**
4176
+ * The index path, if any, of the cell that is currently receiving keyboard focus.
4177
+ *
4178
+ * Setting this property to a valid index path will cause the relevant cell to become highlighted.
4179
+ * Additionally, setting this property to `undefined` will clear the highlighted cell.
4180
+ */
4181
+ _highlightedIndexPath: undefined, // <BMIndexPath<T>, nullable>
4182
+
4183
+ get highlightedIndexPath() {
4184
+ return this._highlightedIndexPath;
4185
+ },
4186
+
4187
+ set highlightedIndexPath(indexPath) {
4188
+ this._highlightIndexPath(indexPath);
4189
+ },
4190
+
4191
+ /**
4192
+ * Highlights the given index path.
4193
+ * @param indexPath <BMIndexPath> The index path to highlight.
4194
+ * {
4195
+ * @param withEvent <UIEvent, nullable> The event that triggered this action, if any.
4196
+ * }
4197
+ */
4198
+ _highlightIndexPath(indexPath, {withEvent: event} = {withEvent: undefined}) {
4199
+ const currentHighlight = this._highlightedIndexPath;
4200
+
4201
+ this._highlightedIndexPath = indexPath;
4202
+
4203
+ // Let the delegate know that the previous cell was dehilighted if any cell was highlighted
4204
+ if (currentHighlight) {
4205
+ if (this.delegate && this.delegate.collectionViewDidDehighlightCellAtIndexPath) {
4206
+ this.delegate.collectionViewDidDehighlightCellAtIndexPath(this, currentHighlight);
4207
+ }
4208
+ }
4209
+
4210
+ if (indexPath) {
4211
+ const cell = this.cellAtIndexPath(indexPath);
4212
+
4213
+ // If the newly highlighted index path isn't visible on screen, attempt to scroll to it
4214
+ if (!cell || !cell._attributes.frame.intersectsRect(this.visibleBounds)) {
4215
+ let shouldScroll = YES;
4216
+
4217
+ if (this.delegate && this.delegate.collectionViewShouldScrollToHighlightedCellAtIndexPath) {
4218
+ shouldScroll = this.delegate.collectionViewShouldScrollToHighlightedCellAtIndexPath(this, indexPath);
4219
+ }
4220
+
4221
+ if (shouldScroll) {
4222
+ this.scrollToCellAtIndexPath(indexPath, {animated: YES});
4223
+ }
4224
+ }
4225
+
4226
+ // If the cell was highlighted, prevent the default action for keyboard events
4227
+ if (event && event instanceof KeyboardEvent) {
4228
+ event.preventDefault();
4229
+ }
4230
+ }
4231
+
4232
+ if (this.delegate && this.delegate.collectionViewDidHighlightCellAtIndexPath) {
4233
+ this.delegate.collectionViewDidHighlightCellAtIndexPath(this, indexPath, {withEvent: event});
4234
+ }
4235
+ },
4236
+
4237
+ /**
4238
+ * Checks if the cell at the specified index path is highlighted.
4239
+ * @param indexPath <BMIndexPath<T>> The index path. This will be loosely compared to the highlighted index path.
4240
+ * @return <Boolean> YES if the cell is highlighted, NO otherwise.
4241
+ */
4242
+ isCellAtIndexPathHighlighted(indexPath) {
4243
+ if (!this._highlightedIndexPath) return NO;
4244
+
4245
+ return this._highlightedIndexPath.isLooselyEqualToIndexPath(indexPath, {usingComparator: this.identityComparator});
4246
+ },
4247
+
4248
+ /**
4249
+ * Invoked when an arrow is pressed while this collection view has keyboard focus.
4250
+ * Highlights the index path to the specified direction of the currently highlighted index path.
4251
+ * @param arrow <String> The key code of the keyboard arrow that was pressed.
4252
+ * {
4253
+ * @param withEvent <KeyboardEvent> The event that triggerred this action.
4254
+ * }
4255
+ */
4256
+ keyboardArrowPressed(arrow, args) {
4257
+ let nextIndexPath = this._highlightedIndexPath;
4258
+ const event = args && args.withEvent;
4259
+
4260
+ if (event) {
4261
+ // If this triggered by an event, ask the delegate if collection view should process this event
4262
+ let canProcess = YES;
4263
+
4264
+ if (this.delegate && this.delegate.collectionViewShouldHighlightCellForArrowKey) {
4265
+ canProcess = this.delegate.collectionViewShouldHighlightCellForArrowKey(this, arrow, {withEvent: event});
4266
+ }
4267
+
4268
+ // If the delegate disallows processing this event, stop
4269
+ if (!canProcess) {
4270
+ return;
4271
+ }
4272
+ }
4273
+
4274
+ do {
4275
+ const startingIndexPath = nextIndexPath;
4276
+
4277
+ if (!nextIndexPath) {
4278
+ // If no index path is currently highlighted, highlight the first index path in the data set.
4279
+ nextIndexPath = this.layout.firstIndexPath();
4280
+ }
4281
+ else {
4282
+ // Otherwise highlight the next appropriate index path.
4283
+ switch (arrow) {
4284
+ case 'ArrowLeft':
4285
+ nextIndexPath = this.layout.indexPathToTheLeftOfIndexPath(nextIndexPath);
4286
+ break;
4287
+ case 'ArrowUp':
4288
+ nextIndexPath = this.layout.indexPathAboveIndexPath(nextIndexPath);
4289
+ break;
4290
+ case 'ArrowRight':
4291
+ nextIndexPath = this.layout.indexPathToTheRightOfIndexPath(nextIndexPath);
4292
+ break;
4293
+ case 'ArrowDown':
4294
+ nextIndexPath = this.layout.indexPathBelowIndexPath(nextIndexPath);
4295
+ break;
4296
+ }
4297
+ }
4298
+
4299
+ // If no index path could be found, don't take any action
4300
+ if (!nextIndexPath) return;
4301
+
4302
+ // Check if this index path can be highlighted
4303
+ let canHighlight = YES;
4304
+ if (this.delegate && this.delegate.collectionViewCanHighlightCellAtIndexPath) {
4305
+ canHighlight = this.delegate.collectionViewCanHighlightCellAtIndexPath(this, nextIndexPath, {withEvent: event});
4306
+ }
4307
+
4308
+ if (canHighlight) {
4309
+ // If the index path can be highlighted continue
4310
+ break;
4311
+ }
4312
+ else {
4313
+ // If the index path can't be highlighted and no appropriate next index path exists don't take any action
4314
+ if (startingIndexPath.isEqualToIndexPath(nextIndexPath, {usingComparator: this.identityComparator})) {
4315
+ return;
4316
+ }
4317
+ }
4318
+
4319
+ // Otherwise look for the next index path
4320
+ } while (nextIndexPath);
4321
+
4322
+ // If this keyboard arrow would highlight the same index path, don't take any action
4323
+ if (nextIndexPath.isEqualToIndexPath(this._highlightedIndexPath, {usingComparator: this.identityComparator})) return;
4324
+
4325
+ // Highlight the index path
4326
+ this._highlightIndexPath(nextIndexPath, {withEvent: event});
4327
+ },
4008
4328
 
4009
4329
 
4010
4330
  /************************************* SELECTION HANDLERS ********************************/
@@ -4061,7 +4381,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
4061
4381
  },
4062
4382
 
4063
4383
  /**
4064
- * Invoked by the collection view during an update to refresh the index paths of the selected cells.
4384
+ * Invoked by the collection view during an update to refresh the index paths of the selected and highlighted cells.
4065
4385
  * Index paths that are no longer in the data set will be removed and all other index paths will be updated
4066
4386
  * to the correct section and row indexes.
4067
4387
  */
@@ -4075,6 +4395,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
4075
4395
 
4076
4396
  if (indexPath) this._selectedIndexPaths.push(indexPath);
4077
4397
  }
4398
+
4399
+ if (this._highlightedIndexPath) {
4400
+ this._highlightedIndexPath = this.dataSet.indexPathForObject(this._highlightedIndexPath.object);
4401
+ }
4078
4402
  },
4079
4403
 
4080
4404
  /**
@@ -5330,10 +5654,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5330
5654
  * Optionally, this scrolling may be animated.
5331
5655
  * @param indexPath <BMIndexPath<T>> The index path of the cell to scroll to.
5332
5656
  * {
5333
- * @param withVerticalGravity <BMCollectionViewScrollingGravityVertical, nullable> Defaults to `BMCollectionViewScrollingGravityVertical.Top`. The scroll gravity to use,
5657
+ * @param withVerticalGravity <BMCollectionViewScrollingGravityVertical, nullable> Defaults to a value depending on where the cell is positioned. The scroll gravity to use,
5334
5658
  * which controls where on the screen the cell should appear after the scrolling operation
5335
5659
  * completes.
5336
- * @param horizontalGravity <BMCollectionViewScrollingGravityHorizontal, nullable> Defaults to `BMCollectionViewScrollingGravityHorizontal.Left`. The scroll gravity to use,
5660
+ * @param horizontalGravity <BMCollectionViewScrollingGravityHorizontal, nullable> Defaults to a value depending on where the cell is positioned. The scroll gravity to use,
5337
5661
  * which controls where on the screen the cell should appear after the scrolling operation
5338
5662
  * completes.
5339
5663
  * @param animated <Boolean, nullable> Defaults to `NO`. If set to `YES`, the scroll will be animated, otherwise it will be
@@ -5343,10 +5667,40 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5343
5667
  */
5344
5668
  scrollToCellAtIndexPath: function (indexPath, options) {
5345
5669
  var rect = this._layout.rectWithScrollingPositionOfCellAtIndexPath(indexPath);
5670
+ let visibleBounds;
5346
5671
 
5347
5672
  if (rect) {
5348
- var verticalGravity = (options && options.withVerticalGravity) || BMCollectionViewScrollingGravityVertical.Top;
5349
- var horizontalGravity = (options && options.horizontalGravity) || BMCollectionViewScrollingGravityHorizontal.Left;
5673
+ var verticalGravity = (options && options.withVerticalGravity);
5674
+ var horizontalGravity = (options && options.horizontalGravity);
5675
+
5676
+ if (!verticalGravity) {
5677
+ visibleBounds = this.visibleBounds;
5678
+
5679
+ if (rect.origin.y > visibleBounds.bottom) {
5680
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Bottom;
5681
+ }
5682
+ else if (rect.bottom < visibleBounds.origin.y) {
5683
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Top;
5684
+ }
5685
+ else {
5686
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Center;
5687
+ }
5688
+ }
5689
+
5690
+ if (!horizontalGravity) {
5691
+ visibleBounds = visibleBounds || this.visibleBounds;
5692
+
5693
+ if (rect.origin.x > visibleBounds.right) {
5694
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Right;
5695
+ }
5696
+ else if (rect.right < visibleBounds.origin.x) {
5697
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Left;
5698
+ }
5699
+ else {
5700
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Center;
5701
+ }
5702
+ }
5703
+
5350
5704
  var animated = options && options.animated;
5351
5705
  return this.scrollToRect(rect, {withVerticalGravity: verticalGravity, horizontalGravity: horizontalGravity, animated: animated});
5352
5706
  }
@@ -5360,10 +5714,10 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5360
5714
  * @param identifier <String> The supplementary view's type identifier.
5361
5715
  * {
5362
5716
  * @param atIndexPath <BMIndexPath<T>> The index path of the supplementary view to scroll to.
5363
- * @param verticalGravity <BMCollectionViewScrollingGravityVertical, nullable> Defaults to BMCollectionViewScrollingGravityVertical.Top. The scroll gravity to use,
5717
+ * @param verticalGravity <BMCollectionViewScrollingGravityVertical, nullable> Defaults to a value depending on where the cell is positioned. The scroll gravity to use,
5364
5718
  * which controls where on the screen the cell should appear after the scrolling operation
5365
5719
  * completes.
5366
- * @param horizontalGravity <BMCollectionViewScrollingGravityHorizontal, nullable> Defaults to BMCollectionViewScrollingGravityHorizontal.Left. The scroll gravity to use,
5720
+ * @param horizontalGravity <BMCollectionViewScrollingGravityHorizontal, nullable> Defaults to a value depending on where the cell is positioned. The scroll gravity to use,
5367
5721
  * which controls where on the screen the cell should appear after the scrolling operation
5368
5722
  * completes.
5369
5723
  * @param animated <Boolean, nullable> Defaults to NO. If set to YES, the scroll will be animated, otherwise it will be
@@ -5373,10 +5727,40 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5373
5727
  */
5374
5728
  scrollToSupplementaryViewWithIdentifier: function (identifier, options) {
5375
5729
  var rect = this._layout.rectWithScrollingPositionOfSupplementaryViewWithIdentifier(identifier, {atIndexPath: options.atIndexPath});
5730
+ let visibleBounds;
5376
5731
 
5377
5732
  if (rect) {
5378
- var verticalGravity = options.verticalGravity || BMCollectionViewScrollingGravityHorizontal.Top;
5379
- var horizontalGravity = options.horizontalGravity || BMCollectionViewScrollingGravityHorizontal.Left;
5733
+ var verticalGravity = options.verticalGravity;
5734
+ var horizontalGravity = options.horizontalGravity;
5735
+
5736
+ if (!verticalGravity) {
5737
+ visibleBounds = this.visibleBounds;
5738
+
5739
+ if (rect.origin.y > visibleBounds.bottom) {
5740
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Bottom;
5741
+ }
5742
+ else if (rect.bottom < visibleBounds.origin.y) {
5743
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Top;
5744
+ }
5745
+ else {
5746
+ verticalGravity = BMCollectionViewScrollingGravityVertical.Center;
5747
+ }
5748
+ }
5749
+
5750
+ if (!horizontalGravity) {
5751
+ visibleBounds = visibleBounds || this.visibleBounds;
5752
+
5753
+ if (rect.origin.x > visibleBounds.right) {
5754
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Right;
5755
+ }
5756
+ else if (rect.right < visibleBounds.origin.x) {
5757
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Left;
5758
+ }
5759
+ else {
5760
+ horizontalGravity = BMCollectionViewScrollingGravityHorizontal.Center;
5761
+ }
5762
+ }
5763
+
5380
5764
  var animated = options.animated;
5381
5765
  return this.scrollToRect(rect, {withVerticalGravity: verticalGravity, horizontalGravity: horizontalGravity, animated: animated});
5382
5766
  }
@@ -5408,7 +5792,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5408
5792
  offset.y = rect.origin.y + rect.size.height / 2 - this.frame.size.height / 2;
5409
5793
  }
5410
5794
  else if (options.withVerticalGravity == BMCollectionViewScrollingGravityVertical.Bottom) {
5411
- offset.y = rect.origin.y + rect.size.height - this.frame.size.height / 2;
5795
+ offset.y = rect.origin.y + rect.size.height - this.frame.size.height;
5412
5796
  }
5413
5797
 
5414
5798
  // Then the horizontal X offset
@@ -5419,7 +5803,7 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5419
5803
  offset.x = rect.origin.x + rect.size.width / 2 - this.frame.size.width / 2;
5420
5804
  }
5421
5805
  else if (options.withVerticalGravity == BMCollectionViewScrollingGravityHorizontal.Right) {
5422
- offset.x = rect.origin.x + rect.size.width - this.frame.size.width / 2;
5806
+ offset.x = rect.origin.x + rect.size.width - this.frame.size.width;
5423
5807
  }
5424
5808
 
5425
5809
  // Scroll to the offset point
@@ -5428,41 +5812,6 @@ BMCollectionView.prototype = BMExtend(BM_COLLECTION_VIEW_USE_BMVIEW_SUBCLASS ? O
5428
5812
  return BMAnimateWithBlock(_ => {
5429
5813
  self.scrollOffset = offset;
5430
5814
  }, {duration: 300});
5431
-
5432
- /*
5433
- // Scrolling is performed differently based on whether iScroll is used or not
5434
- if (this.iScroll) {
5435
- this.iScroll.scrollTo(-(offset.x | 0), -(offset.y | 0), 300, IScroll.utils.ease.quadratic);
5436
-
5437
- // For iScroll, the layout should be refreshed after the animation
5438
- var self = this;
5439
- window.setTimeout(function () {
5440
- self._handleNewScrollFromEvent({x: offset.x, y: offset.y});
5441
- }, 316);
5442
- }
5443
- else {
5444
- // For native scrolling, jQuery animations are used instead
5445
- var startingPoint = this.scrollOffset.copy();
5446
- var targetPoint = offset;
5447
- var self = this;
5448
- this._container.velocity({
5449
- tween: 1
5450
- }, {
5451
- duration: 300,
5452
- queue: NO,
5453
- progress: function (elements, completion) {
5454
- self._container[0].scrollLeft = ((targetPoint.x - startingPoint.x) * completion + startingPoint.x) | 0;
5455
- self._container[0].scrollTop = ((targetPoint.y - startingPoint.y) * completion + startingPoint.y) | 0;
5456
-
5457
- self._handleNewScrollFromEvent(null);
5458
- },
5459
- complete: function () {
5460
-
5461
- self._handleNewScrollFromEvent(null);
5462
- }
5463
- });
5464
- }
5465
- */
5466
5815
  }
5467
5816
  else {
5468
5817
  // Scrolling is performed differently based on whether iScroll is used or not