onKeyDown: function SelectionMode_onKeyDown()

in src/Clients/Web/winjs/js/winjs.js [35948:36203]


                onKeyDown: function SelectionMode_onKeyDown(eventObject) {
                    var that = this,
                        site = this.site,
                        swipeEnabled = site._swipeBehavior === _UI.SwipeBehavior.select,
                        view = site._view,
                        oldEntity = site._selection._getFocused(),
                        handled = true,
                        ctrlKeyDown = eventObject.ctrlKey;

                    function setNewFocus(newEntity, skipSelection, clampToBounds) {
                        function setNewFocusImpl(maxIndex) {
                            var moveView = true,
                                invalidIndex = false;
                            // Since getKeyboardNavigatedItem is purely geometry oriented, it can return us out of bounds numbers, so this check is necessary
                            if (clampToBounds) {
                                newEntity.index = Math.max(0, Math.min(maxIndex, newEntity.index));
                            } else if (newEntity.index < 0 || newEntity.index > maxIndex) {
                                invalidIndex = true;
                            }
                            if (!invalidIndex && (oldEntity.index !== newEntity.index || oldEntity.type !== newEntity.type)) {
                                var changeFocus = dispatchKeyboardNavigating(site._element, oldEntity, newEntity);
                                if (changeFocus) {
                                    moveView = false;

                                    // If the oldEntity is completely off-screen then we mimic the desktop
                                    // behavior. This is consistent with navbar keyboarding.
                                    if (that._setNewFocusItemOffsetPromise) {
                                        that._setNewFocusItemOffsetPromise.cancel();
                                    }
                                    site._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.high, function () {
                                        that._setNewFocusItemOffsetPromise = site._getItemOffset(oldEntity, true).then(function (range) {
                                            range = site._convertFromCanvasCoordinates(range);
                                            var oldItemOffscreen = range.end <= site.scrollPosition || range.begin >= site.scrollPosition + site._getViewportLength() - 1;
                                            that._setNewFocusItemOffsetPromise = site._getItemOffset(newEntity).then(function (range) {
                                                that._setNewFocusItemOffsetPromise = null;
                                                var retVal = {
                                                    position: site.scrollPosition,
                                                    direction: "right"
                                                };
                                                if (oldItemOffscreen) {
                                                    // oldEntity is completely off-screen
                                                    site._selection._setFocused(newEntity, true);
                                                    range = site._convertFromCanvasCoordinates(range);
                                                    if (newEntity.index > oldEntity.index) {
                                                        retVal.direction = "right";
                                                        retVal.position = range.end - site._getViewportLength();
                                                    } else {
                                                        retVal.direction = "left";
                                                        retVal.position = range.begin;
                                                    }
                                                }
                                                site._changeFocus(newEntity, skipSelection, ctrlKeyDown, oldItemOffscreen, true);
                                                if (!oldItemOffscreen) {
                                                    return Promise.cancel;
                                                } else {
                                                    return retVal;
                                                }
                                            }, function (error) {
                                                site._changeFocus(newEntity, skipSelection, ctrlKeyDown, true, true);
                                                return Promise.wrapError(error);
                                            });
                                            return that._setNewFocusItemOffsetPromise;
                                        }, function (error) {
                                            site._changeFocus(newEntity, skipSelection, ctrlKeyDown, true, true);
                                            return Promise.wrapError(error);
                                        });
                                        return that._setNewFocusItemOffsetPromise;
                                    }, true);
                                }
                            }
                            // When a key is pressed, we want to make sure the current focus is in view. If the keypress is changing to a new valid index,
                            // _changeFocus will handle moving the viewport for us. If the focus isn't moving, though, we need to put the view back on
                            // the current item ourselves and call setFocused(oldFocus, true) to make sure that the listview knows the focused item was
                            // focused via keyboard and renders the rectangle appropriately.
                            if (moveView) {
                                site._selection._setFocused(oldEntity, true);
                                site.ensureVisible(oldEntity);
                            }
                            if (invalidIndex) {
                                return { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX };
                            } else {
                                return newEntity;
                            }
                        }

                        // We need to get the final item in the view so that we don't try setting focus out of bounds.
                        if (newEntity.type === _UI.ObjectType.item) {
                            return Promise.wrap(view.lastItemIndex()).then(setNewFocusImpl);
                        } else if (newEntity.type === _UI.ObjectType.groupHeader) {
                            return Promise.wrap(site._groups.length() - 1).then(setNewFocusImpl);
                        } else {
                            return Promise.wrap(0).then(setNewFocusImpl);
                        }
                    }

                    var Key = _ElementUtilities.Key,
                        keyCode = eventObject.keyCode,
                        rtl = site._rtl();

                    if (!this._isInteractive(eventObject.target)) {
                        if (eventObject.ctrlKey && !eventObject.altKey && !eventObject.shiftKey && this._keyboardAcceleratorHandlers[keyCode]) {
                            this._keyboardAcceleratorHandlers[keyCode]();
                        }
                        if (site.itemsReorderable && (!eventObject.ctrlKey && eventObject.altKey && eventObject.shiftKey && oldEntity.type === _UI.ObjectType.item) &&
                            (keyCode === Key.leftArrow || keyCode === Key.rightArrow || keyCode === Key.upArrow || keyCode === Key.downArrow)) {
                            var selection = site._selection,
                                focusedIndex = oldEntity.index,
                                movingUnselectedItem = false,
                                processReorder = true;
                            if (!selection.isEverything()) {
                                if (!selection._isIncluded(focusedIndex)) {
                                    var item = site._view.items.itemAt(focusedIndex);
                                    // Selected items should never be marked as non draggable, so we only need to check for nonDraggableClass when trying to reorder an unselected item.
                                    if (item && _ElementUtilities.hasClass(item, _Constants._nonDraggableClass)) {
                                        processReorder = false;
                                    } else {
                                        movingUnselectedItem = true;
                                        selection = new _SelectionManager._Selection(this.site, [{ firstIndex: focusedIndex, lastIndex: focusedIndex }]);
                                    }
                                }
                                if (processReorder) {
                                    var dropIndex = focusedIndex;
                                    if (keyCode === Key.rightArrow) {
                                        dropIndex += (rtl ? -1 : 1);
                                    } else if (keyCode === Key.leftArrow) {
                                        dropIndex += (rtl ? 1 : -1);
                                    } else if (keyCode === Key.upArrow) {
                                        dropIndex--;
                                    } else {
                                        dropIndex++;
                                    }
                                    // If the dropIndex is larger than the original index, we're trying to move items forward, so the search for the first unselected item to insert after should move forward.
                                    var movingAhead = (dropIndex > focusedIndex),
                                        searchForward = movingAhead;
                                    if (movingAhead && dropIndex >= this.site._cachedCount) {
                                        // If we're at the end of the list and trying to move items forward, dropIndex should be >= cachedCount.
                                        // That doesn't mean we don't have to do any reordering, though. A selection could be broken down into
                                        // a few blocks. We need to make the selection contiguous after this reorder, so we've got to search backwards
                                        // to find the first unselected item, then move everything in the selection after it.
                                        searchForward = false;
                                        dropIndex = this.site._cachedCount - 1;
                                    }
                                    dropIndex = this._findFirstAvailableInsertPoint(selection, dropIndex, searchForward);
                                    dropIndex = Math.min(Math.max(-1, dropIndex), this.site._cachedCount - 1);
                                    var reportedInsertAfterIndex = dropIndex - (movingAhead || dropIndex === -1 ? 0 : 1),
                                        reportedIndex = dropIndex,
                                        groupsEnabled = this.site._groupsEnabled();

                                    if (groupsEnabled) {
                                        // The indices we picked for the index/insertAfterIndex to report in our events is always correct in an ungrouped list,
                                        // and mostly correct in a grouped list. The only problem occurs when you move an item (or items) ahead into a new group,
                                        // or back into a previous group, such that the items should be the first/last in the group. Take this list as an example:
                                        // [Group A] [a] [b] [c] [Group B] [d] [e]
                                        // When [d] is focused, right/down arrow reports index: 4, insertAfterIndex: 4, which is right -- it means move [d] after [e].
                                        // Similarily, when [c] is focused and left/up is pressed, we report index: 1, insertAfterIndex: 0 -- move [c] to after [a].
                                        // Take note that index does not tell us where focus is / what item is being moved.
                                        // Like mouse/touch DnD, index tells us what the dragBetween slots would be were we to animate a dragBetween.
                                        // The problem cases are moving backwards into a previous group, or forward into the next group.
                                        // If [c] were focused and the user pressed right/down, we would report index: 3, insertAfterIndex: 3. In other words, move [c] after [d].
                                        // That's not right at all - [c] needs to become the first element of [Group B]. When we're moving ahead, then, and our dropIndex
                                        // is the first index of a new group, we adjust insertAfterIndex to be dropIndex - 1. Now we'll report index:3, insertAfterIndex: 2, which means
                                        // [c] is now the first element of [Group B], rather than the last element of [Group A]. This is exactly the same as what we would report when
                                        // the user mouse/touch drags [c] right before [d].
                                        // Similarily, when [d] is focused and we press left/up, without the logic below we would report index: 2, insertAfterIndex: 1, so we'd try to move
                                        // [d] ahead of [b]. Again, [d] first needs the opportunity to become the last element in [Group A], so we adjust the insertAfterIndex up by 1.
                                        // We then will report index:2, insertAfterIndex:2, meaning insert [d] in [Group A] after [c], which again mimics the mouse/touch API.
                                        var groups = this.site._groups,
                                            groupIndex = (dropIndex > -1 ? groups.groupFromItem(dropIndex) : 0);
                                        if (movingAhead) {
                                            if (groups.group(groupIndex).startIndex === dropIndex) {
                                                reportedInsertAfterIndex--;
                                            }
                                        } else if (groupIndex < (groups.length() - 1) && dropIndex === (groups.group(groupIndex + 1).startIndex - 1)) {
                                            reportedInsertAfterIndex++;
                                        }
                                    }

                                    if (this._fireDragBetweenEvent(reportedIndex, reportedInsertAfterIndex, null) && this._fireDropEvent(reportedIndex, reportedInsertAfterIndex, null)) {
                                        if (groupsEnabled) {
                                            return;
                                        }

                                        this._reorderItems(dropIndex, selection, movingUnselectedItem, !movingAhead, true);
                                    }
                                }
                            }
                        } else if (!eventObject.altKey) {
                            if (this._keyboardNavigationHandlers[keyCode]) {
                                this._keyboardNavigationHandlers[keyCode](oldEntity).then(function (newEntity) {
                                    var clampToBounds = that._keyboardNavigationHandlers[keyCode].clampToBounds;
                                    if (newEntity.type !== _UI.ObjectType.groupHeader && eventObject.shiftKey && site._selectionAllowed() && site._multiSelection()) {
                                        // Shift selection should work when shift or shift+ctrl are depressed
                                        if (site._selection._pivot === _Constants._INVALID_INDEX) {
                                            site._selection._pivot = oldEntity.index;
                                        }
                                        setNewFocus(newEntity, true, clampToBounds).then(function (newEntity) {
                                            if (newEntity.index !== _Constants._INVALID_INDEX) {
                                                var firstIndex = Math.min(newEntity.index, site._selection._pivot),
                                                    lastIndex = Math.max(newEntity.index, site._selection._pivot),
                                                    additive = (eventObject.ctrlKey || site._tap === _UI.TapBehavior.toggleSelect);
                                                that._selectRange(firstIndex, lastIndex, additive);
                                            }
                                        });
                                    } else {
                                        site._selection._pivot = _Constants._INVALID_INDEX;
                                        setNewFocus(newEntity, false, clampToBounds);
                                    }
                                });
                            } else if (!eventObject.ctrlKey && keyCode === Key.enter) {
                                var element = oldEntity.type === _UI.ObjectType.groupHeader ? site._groups.group(oldEntity.index).header : site._view.items.itemBoxAt(oldEntity.index);
                                if (element) {
                                    if (oldEntity.type === _UI.ObjectType.groupHeader) {
                                        this._pressedHeader = element;
                                        this._pressedItemBox = null;
                                        this._pressedContainer = null;
                                    } else {
                                        this._pressedItemBox = element;
                                        this._pressedContainer = site._view.items.containerAt(oldEntity.index);
                                        this._pressedHeader = null;
                                    }

                                    var allowed = this._verifySelectionAllowed(oldEntity);
                                    if (allowed.canTapSelect) {
                                        this._itemEventsHandler.handleTap(oldEntity);
                                    }
                                    this._fireInvokeEvent(oldEntity, element);
                                }
                            } else if (oldEntity.type !== _UI.ObjectType.groupHeader &&
                                    ((eventObject.ctrlKey && keyCode === Key.enter) ||
                                    (swipeEnabled && eventObject.shiftKey && keyCode === Key.F10) ||
                                    (swipeEnabled && keyCode === Key.menu) ||
                                    keyCode === Key.space)) {
                                // Swipe emulation
                                this._itemEventsHandler.handleSwipeBehavior(oldEntity.index);
                                site._changeFocus(oldEntity, true, ctrlKeyDown, false, true);
                            } else if (keyCode === Key.escape && site._selection.count() > 0) {
                                site._selection._pivot = _Constants._INVALID_INDEX;
                                site._selection.clear();
                            } else {
                                handled = false;
                            }
                        } else {
                            handled = false;
                        }

                        this._keyDownHandled = handled;
                        if (handled) {
                            eventObject.stopPropagation();
                            eventObject.preventDefault();
                        }
                    }

                    if (keyCode === Key.tab) {
                        this.site._keyboardFocusInbound = true;
                    }
                },