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;
}
},