UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,511 lines (1,276 loc) 44.9 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /** * @namespace * @name sap.ui.core.delegate * @public */ // Provides class sap.ui.core.delegate.ItemNavigation sap.ui.define([ 'sap/ui/base/EventProvider', "sap/base/assert", "sap/base/Log", "sap/ui/events/KeyCodes", "sap/ui/thirdparty/jquery", "sap/ui/core/Configuration", "sap/ui/dom/jquery/Selectors" // jQuery custom selectors ":sapFocusable" ], function(EventProvider, assert, Log, KeyCodes, jQuery, Configuration) { "use strict"; /* eslint-disable no-lonely-if */ /** * Creates an <code>ItemNavigation</code> delegate that can be attached to controls requiring * capabilities for keyboard navigation between items. * * @class Delegate for the navigation between DOM nodes with the keyboard. * * The <code>ItemNavigation</code> provides keyboard and mouse navigation between DOM nodes representing items. * This means that controls rendering a list of items can attach this delegate to get a common keyboard and mouse support * to navigate between these items. * It is possible to navigate between the items via the arrow keys. * If needed, paging using the Page Up and Page Down keys is possible. (To activate call <code>setPageSize</code> with a value &gt; 0.) * HOME and END keys are also supported. * Focusing an item via mouse also is also supported. For mouse navigation, the <code>mousedown</code> event is used. * * As the <code>ItemNavigation</code> works with DOM nodes, the items and the control area must be provided as * DOM references. There is one root DOM reference (set via <code>setRootDomRef</code>). * All item DOM references (set via <code>setItemDomRefs</code>) must be places somewhere inside of this root DOM reference. * Only focusable items are used for the navigation, meaning disabled items or separator items are just ignored by navigating * through the items. In some cases however, it makes sense to put the non-focusable items in the array of the DOM references to * keep the indexes stable or like in the calling control. * <b>Hint:</b> To make a DOM reference focusable a <code>tabindex</code> of -1 can be set. * * <b>Note</b> After re-rendering of the control or changing the DOM nodes of the control, the * DOM references of the <code>ItemNavigation</code> must be updated. Then the same item * (corresponding to the index) will get the focus. * * The <code>ItemNavigation</code> adjusts the <code>tabindex</code> of all DOM references relating to the current * focused item. So if the control containing the items gets the focus (e.g. via tab navigation), * it is always the focused items which will be focused. * * <b>Note:</b> If the <code>ItemNavigation</code> is nested in another <code>ItemNavigation</code> * (e.g. <code>SegmentedButton</code> in <code>Toolbar</code>), the <code>RootDomRef</code> will always have <code>tabindex</code> -1. * * Per default the <code>ItemNavigation</code> cycles over the items. * It navigates again to the first item if the Arrow Down or Arrow Right key is pressed while * the last item has the focus. It navigates to the last item if arrow up or * arrow left is pressed while the first item has the focus. * If you want to stop the navigation at the first and last item, * call the <code>setCycling</code> method with a value of <code>false</code>. * * It is possible to have multiple columns in the item navigation. If multiple columns * are used, the keyboard navigation changes. The Arrow Right and Arrow Left keys will take the user to the next or previous * item, and the Arrow Up and Arrow Down keys will navigate the same way but in a vertical direction. * * The <code>ItemNavigation</code> also allows setting a selected index that is used to identify * the selected item. Initially, if no other focus is set, the selected item will be the focused one. * Note that navigating through the items will not change the selected item, only the focus. * (For example a radio group has one selected item.) * * @author SAP SE * * @extends sap.ui.base.EventProvider * * @param {Element} oDomRef The root DOM reference that includes all items * @param {Element[]} aItemDomRefs Array of DOM references representing the items for the navigation * @param {boolean} [bNotInTabChain=false] Whether the selected element should be in the tab chain or not * * @version 1.111.5 * @alias sap.ui.core.delegate.ItemNavigation * @public */ var ItemNavigation = EventProvider.extend("sap.ui.core.delegate.ItemNavigation", /** @lends sap.ui.core.delegate.ItemNavigation.prototype */ { constructor: function(oDomRef, aItemDomRefs, bNotInTabChain) { EventProvider.apply(this); // the surrounding dom ref that is focused initially this.oDomRef = null; if (oDomRef) { this.setRootDomRef(oDomRef); } // the array of dom refs representing the items this.aItemDomRefs = []; if (aItemDomRefs) { this.setItemDomRefs(aItemDomRefs); } // initialize Tabindex this.iTabIndex = -1; // whether the active element should get a tabindex of 0 or -1 this.iActiveTabIndex = bNotInTabChain ? -1 : 0; // the initial focusedindex this.iFocusedIndex = -1; // the initial selected index (if any) this.iSelectedIndex = -1; // default for cycling this.bCycling = true; // default for table mode this.bTableMode = false; // the pagesize for pageup and down events this.iPageSize = -1; // a marker to enable focusin to decide HOW the focus arrived this._bMouseDownHappened = false; // default disabled modifiers these modifiers will not be handled by ItemNavigation this.oDisabledModifiers = { sapend : ["alt", "shift"], saphome : ["alt", "shift"] }; } }); ItemNavigation.Events = { BeforeFocus: "BeforeFocus", AfterFocus: "AfterFocus", BorderReached: "BorderReached", FocusAgain: "FocusAgain", FocusLeave: "FocusLeave" }; /** * The <code>BeforeFocus</code> event is fired before the actual item is focused. * Listeners may prevent the focus by calling the <code>preventDefault</code> method on the event object. * * @name sap.ui.core.delegate.ItemNavigation#BeforeFocus * @event * @param {int} index Index of the item * @param {jQuery.Event} event Event that leads to the focus change * @public */ /** * The <code>AfterFocus</code> event is fired after the actual item is focused. * The control can register to this event and react on the focus change. * * @name sap.ui.core.delegate.ItemNavigation#AfterFocus * @event * @param {int} index Index of the item * @param {jQuery.Event} event Event that leads to the focus change * @public */ /** * The <code>BorderReached</code> event is fired if the border of the items is reached and * no cycling is used, meaning an application can react on this. * * For example if the first item is focused and the Arrow Left key is pressed. * * @name sap.ui.core.delegate.ItemNavigation#BorderReached * @event * @param {int} index Index of the item * @param {jQuery.Event} event Event that leads to the focus change * @public */ /** * The <code>FocusAgain</code> event is fired if the current focused item is focused again * (e.g. click again on focused item.) * * @name sap.ui.core.delegate.ItemNavigation#FocusAgain * @event * @param {int} index Index of the item * @param {jQuery.Event} event Event that leads to the focus change * @public */ /** * The <code>FocusLeave</code> event fired if the focus is set outside the control handled by the <code>ItemNavigation</code>. * * @name sap.ui.core.delegate.ItemNavigation#FocusLeave * @event * @param {int} index Index of the item * @param {jQuery.Event} event Event that leads to the focus change * @public */ /** * Sets the disabled modifiers * These modifiers will not be handled by the <code>ItemNavigation</code> * * <pre> * Example: Disable shift + up handling of the <code>ItemNavigation</code> * * oItemNavigation.setDisabledModifiers({ * sapnext : ["shift"] * }); * * Possible keys are : "shift", "alt", "ctrl", "meta" * Possible events are : "sapnext", "sapprevious", "saphome", "sapend" * </pre> * * @param {Object} oDisabledModifiers Object that includes event type with disabled keys as an array * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setDisabledModifiers = function(oDisabledModifiers) { this.oDisabledModifiers = oDisabledModifiers; return this; }; /** * Returns disabled modifiers * These modifiers will not be handled by the <code>ItemNavigation</code> * * @param {object} oDisabledModifiers Object that includes event type with disabled keys as an array * @return {object} Object that includes event type with disabled keys as an array * @public */ ItemNavigation.prototype.getDisabledModifiers = function(oDisabledModifiers) { return this.oDisabledModifiers; }; /** * Check whether given event has disabled modifier or not * * @param {jQuery.Event} oEvent jQuery event * @return {boolean} Flag if disabled modifiers are set * @public */ ItemNavigation.prototype.hasDisabledModifier = function(oEvent) { var aDisabledKeys = this.oDisabledModifiers[oEvent.type.replace("modifiers", "")]; if (Array.isArray(aDisabledKeys)) { for (var i = 0; i < aDisabledKeys.length; i++) { if (oEvent[aDisabledKeys[i] + "Key"]) { return true; } } } return false; }; /** * Sets the root DOM reference surrounding the items * * @param {Element} oDomRef Root DOM reference * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setRootDomRef = function(oDomRef) { this.oDomRef = oDomRef; // in nested ItemNavigation the tabindex must only be set at the root DOM from the parent ItemNavigation if (!jQuery(this.oDomRef).data("sap.INItem")) { if (this.iFocusedIndex >= 0) { jQuery(this.oDomRef).attr("tabindex", this.iTabIndex); } else { jQuery(this.oDomRef).attr("tabindex", this.iActiveTabIndex); } } jQuery(this.oDomRef).data("sap.INRoot", this); return this; }; /** * Returns the root DOM reference surrounding the items * * @return {Element} Root DOM reference * @public */ ItemNavigation.prototype.getRootDomRef = function() { return this.oDomRef; }; /** * Returns the array of item DOM references * * @return {Element[]} Array of item DOM references * @public */ ItemNavigation.prototype.getItemDomRefs = function() { return this.aItemDomRefs; }; /** * Sets the item DOM references as an array for the items * * @param {Element[]} aItemDomRefs Array of DOM references or DOM node list object, representing the items * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setItemDomRefs = function(aItemDomRefs) { assert(typeof aItemDomRefs === "object" && typeof aItemDomRefs.length === "number", "aItemDomRefs must be an array of DOM elements"); this.aItemDomRefs = aItemDomRefs; // recalculate focused index if (this.iFocusedIndex > -1) { var iItemsLength = aItemDomRefs.length; if (this.iFocusedIndex > iItemsLength - 1) { this.iFocusedIndex = iItemsLength - 1; } var oActiveElement = document.activeElement; if (oActiveElement && oActiveElement != this.aItemDomRefs[this.iFocusedIndex]) { for (var i = 0; i < iItemsLength; i++) { if (oActiveElement == this.aItemDomRefs[i]) { this.iFocusedIndex = i; break; } } } } // in nested ItemNavigation the tabindex must only be set if it's the focused item of the parent ItemNavigation for (var i = 0; i < this.aItemDomRefs.length; i++) { if (this.aItemDomRefs[i]) { // separators return null here var $Item = jQuery(this.aItemDomRefs[i]); // as this code can be executed even if the items are not visible (e.g. Popup is closed) // no focusable check can be performed. So only for the currently focused item // the tabindex is set to 0. For all items with tabindex 0 the tabindex is set to -1 // Items without tabindex are checked for focusable on the first focusin on the root. if (i == this.iFocusedIndex && !$Item.data("sap.INRoot")) { $Item.attr("tabindex", this.iActiveTabIndex); } else if ($Item.attr("tabindex") == "0") { // set tabindex to -1 only if already set to 0 $Item.attr("tabindex", -1); } $Item.data("sap.INItem", true); $Item.data("sap.InNavArea", true); //Item is in navigation area - allow navigation mode and edit mode if ($Item.data("sap.INRoot") && i != this.iFocusedIndex) { // item is root of a nested ItemNavigation -> set tabindexes from its items to -1 $Item.data("sap.INRoot").setNestedItemsTabindex(); } } } return this; }; /** * Sets the <code>tabindex</code> of the items. * * This cannot be done while setting items because at this point of time the items might * be invisible (because e.g. a popup closed), meaning the focusable check will fail. * So the items <code>tabindex</code>es are set if the rootDom is focused the first time. * * @return {this} <code>this</code> to allow method chaining * @private */ ItemNavigation.prototype.setItemsTabindex = function() { for (var i = 0; i < this.aItemDomRefs.length; i++) { if (this.aItemDomRefs[i]) { // separators return null here var $Item = jQuery(this.aItemDomRefs[i]); if ($Item.is(":sapFocusable")) { // not focusable items (like labels) must not get a tabindex attribute if (i == this.iFocusedIndex && !$Item.data("sap.INRoot")) { $Item.attr("tabindex", this.iActiveTabIndex); } else { $Item.attr("tabindex", -1); } } } } return this; }; /** * Sets <code>tabindex</code> of item to -1 * if called from parent <code>ItemNavigation</code> if ItemNavigation is nested. * In the nested case the <code>tabindex</code> is ruled by the parent <code>ItemNavigation</code>, * only the top items can have <code>tabindex</code> = 0. * * @return {this} <code>this</code> to allow method chaining * @private */ ItemNavigation.prototype.setNestedItemsTabindex = function() { if (jQuery(this.oDomRef).data("sap.INItem")) { for (var i = 0; i < this.aItemDomRefs.length; i++) { if (this.aItemDomRefs[i] && jQuery(this.aItemDomRefs[i]).attr("tabindex") == "0") { // separators return null here jQuery(this.aItemDomRefs[i]).attr("tabindex", -1); } } } return this; }; /** * Destroys the delegate and releases all DOM references. * */ ItemNavigation.prototype.destroy = function() { if (this.oDomRef) { jQuery(this.oDomRef).removeData("sap.INRoot"); this.oDomRef = null; } if (this.aItemDomRefs) { for (var i = 0; i < this.aItemDomRefs.length; i++) { if (this.aItemDomRefs[i]) { // separators return null here jQuery(this.aItemDomRefs[i]).removeData("sap.INItem"); jQuery(this.aItemDomRefs[i]).removeData("sap.InNavArea"); } } this.aItemDomRefs = null; } this._bItemTabIndex = undefined; this.iFocusedIndex = -1; }; /** * Sets whether the <code>ItemNavigation</code> should cycle through the items. * * If cycling is disabled the navigation stops at the first and last item, if the corresponding arrow keys are used. * * @param {boolean} bCycling Set to true if cycling should be done, else false * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setCycling = function(bCycling) { this.bCycling = bCycling; return this; }; /** * Sets whether the <code>ItemNavigation</code> should use the table mode to navigate through * the items (navigation in a grid). * * @param {boolean} bTableMode Set to true if table mode should be used, else false * @param {boolean} [bTableList] This sets a different behavior for table mode. * In this mode we keep using table navigation but there are some differences. e.g. * <ul> * <li>Page-up moves focus to the first row, not to the first cell like in table mode</li> * <li>Page-down moves focus to the last row, not to the last cell like in table mode</li> * </ul> * * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setTableMode = function(bTableMode, bTableList) { this.bTableMode = bTableMode; if (this.oConfiguration === undefined) { this.oConfiguration = Configuration; } this.bTableList = bTableMode ? bTableList : false; return this; }; /** * Sets the page size of the item navigation to allow Page Up and Page Down keys. * * @param {int} iPageSize The page size, needs to be at least 1 * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setPageSize = function(iPageSize) { this.iPageSize = iPageSize; return this; }; /** * Sets the selected index if the used control supports selection. * * @param {int} iIndex Index of the first selected item * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setSelectedIndex = function(iIndex) { this.iSelectedIndex = iIndex; return this; }; /** * Sets whether the items are displayed in columns. * * If columns are used, the Arrow Up and Arrow Down keys navigate to the next or previous * item of the column. If the first or last item of the column is reached, the next focused * item is then in the next or previous column. * * @param {int} iColumns Count of columns for the table mode or cycling mode * @param {boolean} bNoColumnChange Forbids jumping to an other column with Arrow Up and Arrow Down keys * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setColumns = function(iColumns, bNoColumnChange) { this.iColumns = iColumns; this.bNoColumnChange = bNoColumnChange; return this; }; /** * Sets behavior of HOME and END keys if columns are used. * * @param {boolean} bStayInRow HOME -> go to first item in row; END -> go to last item in row * @param {boolean} bCtrlEnabled HOME/END with CTRL -> go to first/last item of all * @return {this} <code>this</code> to allow method chaining * @public */ ItemNavigation.prototype.setHomeEndColumnMode = function(bStayInRow, bCtrlEnabled) { this._bStayInRow = bStayInRow; this._bCtrlEnabled = bCtrlEnabled; return this; }; /** * Sets the focus to the item with the given index. * * @param {int} iIndex Index of the item to focus * @param {jQuery.Event} oEvent Event that leads to focus change * @param {boolean} bPreventScroll Whether scrolling should be prevented when focusing the DOM element * @private */ ItemNavigation.prototype.focusItem = function(iIndex, oEvent, bPreventScroll) { Log.info("FocusItem: " + iIndex + " iFocusedIndex: " + this.iFocusedIndex, "focusItem", "ItemNavigation"); if (iIndex == this.iFocusedIndex && this.aItemDomRefs[this.iFocusedIndex] == document.activeElement) { this.fireEvent(ItemNavigation.Events.FocusAgain, { index: iIndex, event: oEvent }); return; // item already focused -> nothing to do } // if there is no item to put the focus on, we don't even try it, if working in table mode we just focus the next item if (!this.aItemDomRefs[iIndex] || !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")) { if (this.bTableMode) { var iCol = iIndex % this.iColumns; var iOldIndex = iIndex; if (oEvent && oEvent.keyCode == KeyCodes.ARROW_RIGHT) { if (iCol < this.iColumns - 1) { iIndex += this.oConfiguration.getRTL() ? -1 : 1; } } else if (oEvent && oEvent.keyCode == KeyCodes.ARROW_LEFT) { if (iCol > 1) { iIndex -= this.oConfiguration.getRTL() ? -1 : 1; } } else { if (iCol > 1) { iIndex -= 1; } } if (iIndex != iOldIndex) { this.focusItem(iIndex, oEvent); } } return; } if (!this.fireEvent(ItemNavigation.Events.BeforeFocus, { index: iIndex, event: oEvent }, /* bAllowPreventDefault */ true)) { Log.info("Focus prevented on ID: " + this.aItemDomRefs[this.iFocusedIndex].id, "focusItem", "ItemNavigation"); return; } this.setFocusedIndex(iIndex); this.bISetFocus = true; if (oEvent && jQuery(this.aItemDomRefs[this.iFocusedIndex]).data("sap.INRoot")) { // store event type for nested ItemNavigations var oItemItemNavigation = jQuery(this.aItemDomRefs[this.iFocusedIndex]).data("sap.INRoot"); oItemItemNavigation._sFocusEvent = oEvent.type; } Log.info("Set Focus on ID: " + this.aItemDomRefs[this.iFocusedIndex].id, "focusItem", "ItemNavigation"); this.aItemDomRefs[this.iFocusedIndex].focus({ preventScroll: bPreventScroll }); this.fireEvent(ItemNavigation.Events.AfterFocus, { index: iIndex, event: oEvent }); }; /** * Sets the focused index to the given index. * * @param {int} iIndex Index of the item * @return {this} <code>this</code> to allow method chaining * @private */ ItemNavigation.prototype.setFocusedIndex = function(iIndex) { var $Item; if (this.aItemDomRefs.length < 0) { // no items -> don't change TabIndex this.iFocusedIndex = -1; return this; } if (iIndex < 0) { iIndex = 0; } if (iIndex > this.aItemDomRefs.length - 1) { iIndex = this.aItemDomRefs.length - 1; } jQuery(this.oDomRef).attr("tabindex", this.iTabIndex); if (this.iFocusedIndex !== -1 && this.aItemDomRefs.length > this.iFocusedIndex) { jQuery(this.aItemDomRefs[this.iFocusedIndex]).attr("tabindex", -1); // if focus is in nested ItemNavigation but is moved to an other item, remove tabindex from nested item $Item = jQuery(this.aItemDomRefs[this.iFocusedIndex]); if ($Item.data("sap.INRoot") && iIndex != this.iFocusedIndex) { jQuery($Item.data("sap.INRoot").aItemDomRefs[$Item.data("sap.INRoot").iFocusedIndex]).attr("tabindex", -1); } } this.iFocusedIndex = iIndex; var oFocusItem = this.aItemDomRefs[this.iFocusedIndex]; $Item = jQuery(this.aItemDomRefs[this.iFocusedIndex]); if (!$Item.data("sap.INRoot")) { // in nested ItemNavigation the nested item gets the tabindex jQuery(oFocusItem).attr("tabindex", this.iActiveTabIndex); } return this; }; /** * Returns the focused DOM reference. * * @return {Element} Focused DOM reference * @private */ ItemNavigation.prototype.getFocusedDomRef = function() { return this.aItemDomRefs[this.iFocusedIndex]; }; /** * Returns index of the focused item. * * @return {int} focused index * @private */ ItemNavigation.prototype.getFocusedIndex = function() { return this.iFocusedIndex; }; /** * Handles the onfocusin event. * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onfocusin = function(oEvent) { var oSource = oEvent.target; var i = 0; if (oSource == this.oDomRef) { // focus occured on the main dom ref // on first focus - set tabindex for items if (!this._bItemTabIndex) { this.setItemsTabindex(); this._bItemTabIndex = true; } // if the focus came by clicking AND it did not target one of the elements, but the root element, do nothing // (otherwise clicking the scrollbar re-focuses the previously focused element, which causes the browser to scroll it into view) if (this._bMouseDownHappened) { return; } var iIndex; if (jQuery(this.oDomRef).data("sap.INItem") && this._sFocusEvent && !jQuery(this.oDomRef).data("sap.InNavArea")) { // if nested ItemNavigation need to know if focused by parent ItemNavigation (not in Navigation mode) switch (this._sFocusEvent) { case "sapnext": iIndex = 0; break; case "sapprevious": iIndex = this.aItemDomRefs.length - 1; break; default: if (this.iSelectedIndex != -1) { iIndex = this.iSelectedIndex; } else if (this.iFocusedIndex != -1) { iIndex = this.iFocusedIndex; } else { iIndex = 0; } break; } this._sFocusEvent = undefined; } else { if (this.iSelectedIndex != -1) { iIndex = this.iSelectedIndex; } else if (this.iFocusedIndex != -1) { iIndex = this.iFocusedIndex; } else { iIndex = 0; } } this.focusItem(iIndex, oEvent); if (this.iFocusedIndex == -1) { // no item focused, maybe not focusable -> try the next one for (i = iIndex + 1; i < this.aItemDomRefs.length; i++) { this.focusItem(i, oEvent); if (this.iFocusedIndex == i) { break; } } if (this.iFocusedIndex == -1 && iIndex > 0) { // still no item selected, try to find a previous one for (i = iIndex - 1; i >= 0; i--) { this.focusItem(i, oEvent); if (this.iFocusedIndex == i) { break; } } } } // cancel the bubbling of event in this case oEvent.preventDefault(); oEvent.stopPropagation(); } else if (!this.bISetFocus) { // check if this is really the already focused item // in case of a click on a label this could be an other item if (this.aItemDomRefs && oEvent.target != this.aItemDomRefs[this.iFocusedIndex]) { for (i = 0; i < this.aItemDomRefs.length; i++) { if (oEvent.target == this.aItemDomRefs[i]) { this.focusItem(i, oEvent); break; } } } else { // give focused item to registered application this.fireEvent(ItemNavigation.Events.AfterFocus,{index:this.iFocusedIndex, event:oEvent}); } } this.bISetFocus = false; }; /** * Handles the onsapfocusleave event * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapfocusleave = function(oEvent) { if (!oEvent.relatedControlId || !this.oDomRef || !this.oDomRef.contains(sap.ui.getCore().byId(oEvent.relatedControlId).getFocusDomRef())) { // entirely leaving the control handled by this ItemNavigation instance var iIndex; if (this.iSelectedIndex != -1) { iIndex = this.iSelectedIndex; } else if (this.iFocusedIndex != -1) { iIndex = this.iFocusedIndex; } else { iIndex = 0; } this.setFocusedIndex(iIndex); var $DomRef; if (jQuery(this.oDomRef).data("sap.INItem")) { // if in nested ItemNavigation and focus moves to item of parent ItemNavigation -> do not set Tabindex var oParentDomRef; $DomRef = jQuery(this.oDomRef); while (!oParentDomRef) { $DomRef = $DomRef.parent(); if ($DomRef.data("sap.INRoot")) { oParentDomRef = $DomRef.get(0); } } if (!oEvent.relatedControlId || oParentDomRef.contains(sap.ui.getCore().byId(oEvent.relatedControlId).getFocusDomRef())) { jQuery(this.aItemDomRefs[this.iFocusedIndex]).attr("tabindex", -1); } } $DomRef = jQuery(this.oDomRef); if ($DomRef.data("sap.InNavArea") === false) { // check for false to avoid undefinded // if in action mode switch back to navigation mode $DomRef.data("sap.InNavArea", true); } this.fireEvent(ItemNavigation.Events.FocusLeave, { index: iIndex, event: oEvent }); } }; /** * Handles the onmousedown event * Sets the focus to the item if it occured on an item * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onmousedown = function(oEvent) { // set the focus to the clicked element or back to the last var oSource = oEvent.target; var checkFocusableParent = function(oDomRef, oItem) { // as table cell might have focusable content that have not focusable DOM insinde // the table cell should not get the focus but the focusable element inside var bFocusableParent = false; var $CheckDom = jQuery(oDomRef); while (!$CheckDom.is(":sapFocusable") && $CheckDom.get(0) != oItem) { $CheckDom = $CheckDom.parent(); } if ($CheckDom.get(0) != oItem) { // focusable Dom found inside item bFocusableParent = true; } return bFocusableParent; }; if (this.oDomRef && this.oDomRef.contains(oSource)) { // the mouse down occured inside the main dom ref for (var i = 0; i < this.aItemDomRefs.length; i++) { var oItem = this.aItemDomRefs[i]; if (oItem && oItem.contains(oSource)) { if (!this.bTableMode) { // the mousedown occured inside of an item // prevent scrolling when setting focus to the DOM element to make sure // that the "mouseup" event is fired on the same DOM element this.focusItem(i, oEvent, true /* prevent scrolling*/); // no oEvent.preventDefault(); because cursor will not be set in Textfield // no oEvent.stopPropagation(); because e.g. DatePicker can not close popup } else { // only focus the items if the click did not happen on a // focusable element! if (oItem === oSource || !checkFocusableParent(oSource, oItem)) { // prevent scrolling when setting focus to the DOM element to make sure // that the "mouseup" event is fired on the same DOM element this.focusItem(i, oEvent, true /* prevent scrolling*/); // the table mode requires not to prevent the default // behavior on click since we want to allow text selection // click into the control, ... } } return; } } if (oSource == this.oDomRef) { // root DomRef of item navigation has been clicked // focus will also come in a moment - let it know that it was the mouse who caused the focus this._bMouseDownHappened = true; var that = this; window.setTimeout( function(){ that._bMouseDownHappened = false; }, 20 ); } } }; /** * Handles the onsapnext event * Sets the focus to the next item * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapnext = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content return; } if (jQuery(this.oDomRef).data("sap.InNavArea")) { // control is in navigation mode -> no ItemNavigation return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } // focus the next item var iIndex = this.iFocusedIndex, bFirstTime = true, bBorderReached = false; if (iIndex > -1) { if (this.bTableMode) { var iRowCount = this.aItemDomRefs.length / this.iColumns, iRow = Math.floor(iIndex / this.iColumns), iCol = iIndex % this.iColumns; if (oEvent.keyCode == KeyCodes.ARROW_DOWN) { if (iRow < iRowCount - 1) { iIndex += this.iColumns; } } else { if (iCol < this.iColumns - 1) { iIndex += 1; } } } else { do { if (this.iColumns > 1 && oEvent.keyCode == KeyCodes.ARROW_DOWN) { if ((iIndex + this.iColumns) >= this.aItemDomRefs.length) { if (!this.bNoColumnChange) { // on bottom -> begin on top of next column if ((iIndex % this.iColumns) < (this.iColumns - 1)) { iIndex = (iIndex % this.iColumns) + 1; } else if (this.bCycling) { iIndex = 0; } } else { // do not go to an other item; iIndex = this.iFocusedIndex; bBorderReached = true; } } else { iIndex = iIndex + this.iColumns; } } else { if (iIndex == this.aItemDomRefs.length - 1) { // last item if (jQuery(this.oDomRef).data("sap.INItem")) { // if nested ItemNavigations leave here to ItemNavigation of parent return; } else if (this.bCycling) { // in cycling case focus first one, if not - don't change iIndex = 0; } else { // last one, no next item, so leave focus on already focused item (to prevent endless loops) iIndex = this.iFocusedIndex; bBorderReached = true; } } else { iIndex++; } } if (iIndex === this.iFocusedIndex) { if (bFirstTime) { bFirstTime = false; } else { throw new Error("ItemNavigation has no visible/existing items and is hence unable to select the next one"); } } // if item is not visible or a dummy item go to the next one // !jQuery(this.aItemDomRefs[iIndex]).is(":visible") and jQuery(this.aItemDomRefs[iIndex]).css("visibility") === "hidden" // - is not needed as .is(":sapFocusable") do these checks already } while (!this.aItemDomRefs[iIndex] || !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")); } this.focusItem(iIndex, oEvent); if (bBorderReached) { this.fireEvent(ItemNavigation.Events.BorderReached, { index: iIndex, event: oEvent }); } // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); } }; /** * Ensure the sapnext event with modifiers is also handled * * @see #onsapnext * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapnextmodifiers = function(oEvent) { if (this.hasDisabledModifier(oEvent)) { return; } this.onsapnext(oEvent); }; /** * Handles the onsapprevious event * Sets the focus to the previous item * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapprevious = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content return; } if (jQuery(this.oDomRef).data("sap.InNavArea")) { // control is in navigation mode -> no ItemNavigation return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } // focus the previous item var iIndex = this.iFocusedIndex, bFirstTime = true, bBorderReached = false; var iCol = 0; if (iIndex > -1) { if (this.bTableMode) { var iRow = Math.floor(iIndex / this.iColumns); iCol = iIndex % this.iColumns; if (oEvent.keyCode == KeyCodes.ARROW_UP) { if (iRow > 0) { iIndex -= this.iColumns; } } else { if (iCol > 0) { iIndex -= 1; } } } else { do { if (this.iColumns > 1 && oEvent.keyCode == KeyCodes.ARROW_UP) { if ((iIndex - this.iColumns) < 0) { if (!this.bNoColumnChange) { // on top -> begin on end of previous column iCol = 0; if ((iIndex % this.iColumns) > 0) { iCol = (iIndex % this.iColumns) - 1; } else if (this.bCycling) { iCol = Math.min(this.iColumns - 1, this.aItemDomRefs.length - 1); } if (iIndex === 0 && iCol === 0) { iIndex = 0; } else { var iRows = Math.ceil(this.aItemDomRefs.length / this.iColumns); iIndex = iCol + ((iRows - 1) * this.iColumns); if (iIndex >= this.aItemDomRefs.length) { iIndex = iIndex - this.iColumns; } } } else { // do not go to an other item; iIndex = this.iFocusedIndex; bBorderReached = true; } } else { iIndex = iIndex - this.iColumns; } } else { if (iIndex == 0) { // first item if (jQuery(this.oDomRef).data("sap.INItem")) { // if nested ItemNavigations leave here to ItemNavigation of parent return; } else if (this.bCycling) { // in cycling case focus last one, if not - don't change iIndex = this.aItemDomRefs.length - 1; } else { // first one, no next item, so leave focus on already focused item (to prevent endless loops) iIndex = this.iFocusedIndex; bBorderReached = true; } } else { iIndex--; } } if (iIndex == this.iFocusedIndex) { if (bFirstTime) { bFirstTime = false; } else { throw new Error("ItemNavigation has no visible/existing items and is hence unable to select the previous one"); } } // if item is not visible or a dummy item go to the next one } while (!this.aItemDomRefs[iIndex] || !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")); } this.focusItem(iIndex, oEvent); if (bBorderReached) { this.fireEvent(ItemNavigation.Events.BorderReached, { index: iIndex, event: oEvent }); } // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); } }; /** * Ensure the sapprevious event with modifiers is also handled * * @see #onsapprevious * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsappreviousmodifiers = function(oEvent) { if (this.hasDisabledModifier(oEvent)) { return; } this.onsapprevious(oEvent); }; /** * Handles the onsappageup event * Sets the focus to the previous page item of <code>iPageSize</code> > 0 * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsappageup = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } var iIndex = 0; var bBorderReached = false; if (this.iPageSize > 0) { iIndex = this.iFocusedIndex; if (iIndex > -1) { iIndex = iIndex - this.iPageSize; while (iIndex > 0 && !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")) { iIndex--; } if (iIndex < 0) { if (!this.bNoColumnChange) { iIndex = 0; } else { iIndex = this.iFocusedIndex; bBorderReached = true; } } this.focusItem(iIndex, oEvent); } } else if (this.bTableMode) { iIndex = this.iFocusedIndex % this.iColumns; this.focusItem(iIndex, oEvent); } if (bBorderReached) { this.fireEvent(ItemNavigation.Events.BorderReached, { index: iIndex, event: oEvent }); } // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Handles the onsappagedown event. * * Sets the focus to the previous page item of <code>iPageSize</code> > 0 * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsappagedown = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } var iIndex = 0; var bBorderReached = false; if (this.iPageSize > 0) { iIndex = this.iFocusedIndex; if (iIndex > -1) { iIndex = iIndex + this.iPageSize; while (iIndex < this.aItemDomRefs.length - 1 && !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")) { iIndex++; } if (iIndex > this.aItemDomRefs.length - 1) { if (!this.bNoColumnChange) { iIndex = this.aItemDomRefs.length - 1; } else { iIndex = this.iFocusedIndex; bBorderReached = true; } } this.focusItem(iIndex, oEvent); } } else if (this.bTableMode) { var iRowCount = this.aItemDomRefs.length / this.iColumns, iCol = this.iFocusedIndex % this.iColumns; iIndex = (iRowCount - 1) * this.iColumns + iCol; this.focusItem(iIndex, oEvent); } if (bBorderReached) { this.fireEvent(ItemNavigation.Events.BorderReached, { index: iIndex, event: oEvent }); } // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Handles the onsaphome event * * Sets the focus to first visible and focusable item * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsaphome = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content // or shift or alt key is pressed return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } var iIndex = 0; var iRow = 0; if (this.bTableMode) { if (!this.bTableList && !(oEvent.metaKey || oEvent.ctrlKey)) { iRow = Math.floor(this.iFocusedIndex / this.iColumns); iIndex = iRow * this.iColumns; } } else { if ((oEvent.metaKey || oEvent.ctrlKey) && !this._bCtrlEnabled) { // do not handle ctrl return; } if (this._bStayInRow && !(this._bCtrlEnabled && (oEvent.metaKey || oEvent.ctrlKey)) && this.iColumns > 0) { iRow = Math.floor(this.iFocusedIndex / this.iColumns); iIndex = iRow * this.iColumns; } else { while (!this.aItemDomRefs[iIndex] || !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")) { iIndex++; if (iIndex == this.aItemDomRefs.length) { // no visible item -> no new focus return; } } } } this.focusItem(iIndex, oEvent); // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Ensure the sapprevious event with modifiers is also handled * * @see #onsaphome * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsaphomemodifiers = function(oEvent) { if (this.hasDisabledModifier(oEvent)) { return; } this.onsaphome(oEvent); }; /** * Handles the onsapend event * * Sets the focus to last visible and focusable item * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapend = function(oEvent) { if (!this.oDomRef || !this.oDomRef.contains(oEvent.target)) { // current element is not part of the navigation content // or shift or alt key is pressed return; } // in the table mode we only react on events of the domrefs if (this.bTableMode && this.aItemDomRefs.indexOf(oEvent.target) === -1) { return; } var iIndex = this.aItemDomRefs.length - 1; var iRow = 0; if (this.bTableMode) { if (!this.bTableList && !(oEvent.metaKey || oEvent.ctrlKey)) { iRow = Math.floor(this.iFocusedIndex / this.iColumns); iIndex = iRow * this.iColumns + this.iColumns - 1; } } else { if ((oEvent.metaKey || oEvent.ctrlKey) && !this._bCtrlEnabled) { // do not handle ctrl return; } if (this._bStayInRow && !(this._bCtrlEnabled && (oEvent.metaKey || oEvent.ctrlKey)) && this.iColumns > 0) { iRow = Math.floor(this.iFocusedIndex / this.iColumns); iIndex = (iRow + 1) * this.iColumns - 1; if (iIndex >= this.aItemDomRefs.length) { // item not exist -> use last one iIndex = this.aItemDomRefs.length - 1; } } else { while (!this.aItemDomRefs[iIndex] || !jQuery(this.aItemDomRefs[iIndex]).is(":sapFocusable")) { iIndex--; if (iIndex < 0) { // no visible item -> no new focus return; } } } } this.focusItem(iIndex, oEvent); // cancel the event otherwise the browser will scroll oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Ensure the sapprevious event with modifiers is also handled. * * @see #onsapend * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onsapendmodifiers = function(oEvent) { if (this.hasDisabledModifier(oEvent)) { return; } this.onsapend(oEvent); }; /** * Sets <code>tabindex</code> of the root DOM reference to 0. * Is used, for example in image map for IE browser in order to avoid <code>tabindex</code> -1 on image * with which it would not be tabable at all. * * @private */ ItemNavigation.prototype.setTabIndex0 = function() { this.iTabIndex = 0; this.iActiveTabIndex = 0; }; /** * toggle navigation/action mode on F2 * * @param {jQuery.Event} oEvent the browser event * @private */ ItemNavigation.prototype.onkeyup = function(oEvent) { if (oEvent.keyCode == KeyCodes.F2) { var $DomRef = jQuery(this.oDomRef); if ($DomRef.data("sap.InNavArea")) { $DomRef.data("sap.InNavArea", false); } else if ($DomRef.data("sap.InNavArea") === false) { // check for false to avoid undefined $DomRef.data("sap.InNavArea", true); } oEvent.preventDefault(); oEvent.stopPropagation(); } }; return ItemNavigation; });