@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
431 lines (386 loc) • 14.5 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.ui.core.FocusHandler
sap.ui.define([
"../base/EventProvider",
"../base/Object",
"sap/base/Log",
"sap/ui/core/UIAreaRegistry",
"sap/ui/thirdparty/jquery",
"sap/ui/dom/_ready"
],
function (EventProvider, BaseObject, Log, UIAreaRegistry, jQuery, _ready) {
"use strict";
// Element, UIArea module references, lazily probed when needed
var Element;
var UIArea;
var StaticArea;
var oFocusInfoEventProvider = new EventProvider();
var FOCUS_INFO_EVENT = "focusInfo";
var oEventData = {};
/**
* Constructs an instance of an sap.ui.core.FocusHandler.
* Keeps track of the focused element.
*
* @class Keeps track of the focused element.
* @param {Element} oRootRef e.g. document.body
* @alias sap.ui.core.FocusHandler
* @extends sap.ui.base.Object
* @private
*/
var FocusHandler = BaseObject.extend("sap.ui.core.FocusHandler", /** @lends sap.ui.core.FocusHandler.prototype */ {
constructor : function() {
BaseObject.apply(this);
// keep track of element currently in focus
this.oCurrent = null;
// keep track of the element previously had the focus
this.oLast = null;
// buffer the focus/blur events for correct order
this.aEventQueue = [];
// keep track of last focused element
this.oLastFocusedControlInfo = null;
// keep track of focused element which is using Renderer.apiVersion=2
this.oPatchingControlFocusInfo = null;
this.fnEventHandler = this.onEvent.bind(this);
// initialize event handling
_ready().then(function() {
var oRootRef = document.body;
oRootRef.addEventListener("focus", this.fnEventHandler, true);
oRootRef.addEventListener("blur", this.fnEventHandler, true);
Log.debug("FocusHandler setup on Root " + oRootRef.type + (oRootRef.id ? ": " + oRootRef.id : ""), null, "sap.ui.core.FocusHandler");
}.bind(this));
}
});
/**
* Returns the focus info of the current focused control or the control with the given id, if exists.
*
* @see sap.ui.core.FocusHandler#restoreFocus
* @param {string} [sControlId] the id of the control. If not given the id of the current focused control (if exists) is used
* @return {object} the focus info of the current focused control or the control with the given id, if exists.
* @private
*/
FocusHandler.prototype.getControlFocusInfo = function(sControlId){
var oControl;
Element ??= sap.ui.require("sap/ui/core/Element");
sControlId = sControlId || Element?.getActiveElement()?.getId();
if (!sControlId) {
return null;
}
oControl = getControlById(sControlId);
if (oControl) {
return {
id : sControlId,
control : oControl,
info : oControl.getFocusInfo(),
type : oControl.getMetadata().getName(),
focusref : oControl.getFocusDomRef()
};
}
return null;
};
/**
* Stores the focus info of the current focused control which is using Renderer.apiVersion=2
*
* @see sap.ui.core.FocusHandler#restoreFocus
* @see sap.ui.core.FocusHandler#getControlFocusInfo
* @param {HTMLElement} oDomRef The DOM reference of the control where the rendering is happening
* @private
*/
FocusHandler.prototype.storePatchingControlFocusInfo = function(oDomRef) {
var oActiveElement = document.activeElement;
if (!oActiveElement || !oDomRef.contains(oActiveElement)) {
this.oPatchingControlFocusInfo = null;
} else {
this.oPatchingControlFocusInfo = this.getControlFocusInfo();
if (this.oPatchingControlFocusInfo) {
this.oPatchingControlFocusInfo.patching = true;
}
}
};
/**
* Returns the focus info of the last focused control which is using Renderer.apiVersion=2
*
* @see sap.ui.core.FocusHandler#storePatchingControlFocusInfo
* @private
*/
FocusHandler.prototype.getPatchingControlFocusInfo = function() {
return this.oPatchingControlFocusInfo;
};
/**
* If the given control is the last known focused control, the stored focusInfo is updated.
*
* @see sap.ui.core.FocusHandler#restoreFocus
* @see sap.ui.core.FocusHandler#getControlFocusInfo
* @param {string} oControl the control
* @private
*/
FocusHandler.prototype.updateControlFocusInfo = function(oControl){
if (oControl && this.oLastFocusedControlInfo && this.oLastFocusedControlInfo.control === oControl) {
var sControlId = oControl.getId();
this.oLastFocusedControlInfo = this.getControlFocusInfo(sControlId);
Log.debug("Update focus info of control " + sControlId, null, "sap.ui.core.FocusHandler");
}
};
/**
* Adds the given function as an extender of the focus info. The given function will be called within the
* <code>restoreFocus</code> function before the focus info is forwarded to the corresponding control.
*
* @see sap.ui.core.FocusHandler#restoreFocus
* @param {function} fnFunction The function that will be called to extend the focus info
* @param {object} oListener An object which is set as "this" context when callin the "fnFunction"
* @return {sap.ui.core.FocusHandler} The object itself to allow function chaining
* @private
*/
FocusHandler.prototype.addFocusInfoExtender = function(fnFunction, oListener) {
oFocusInfoEventProvider.attachEvent(FOCUS_INFO_EVENT, oEventData, fnFunction, oListener);
return this;
};
/**
* Removes the given function from being an extender of the focus info.
*
* @param {function} fnFunction The function that will be removed
* @param {object} oListener An object which is set as "this" context when callin the "fnFunction". Only when
* the same "oListener" is given as the one that is used to call <code>addFocusInfoExtender</code>, the function
* can be removed correctly.
* @return {sap.ui.core.FocusHandler} The object itself to allow function chaining
* @private
*/
FocusHandler.prototype.removeFocusInfoExtender = function(fnFunction, oListener) {
oFocusInfoEventProvider.detachEvent(FOCUS_INFO_EVENT, fnFunction, oListener);
return this;
};
/**
* Restores the focus to the last known focused control or to the given focusInfo, if possible.
*
* @see sap.ui.core.FocusHandler#getControlFocusInfo
* @param {object} [oControlFocusInfo] the focus info previously received from getControlFocusInfo
* @private
*/
FocusHandler.prototype.restoreFocus = function(oControlFocusInfo){
var oInfo = oControlFocusInfo || this.oLastFocusedControlInfo;
if (!oInfo) {
return;
}
var oControl = getControlById(oInfo.id);
var oFocusRef = oInfo.focusref;
if (oControl
&& oInfo.info
&& oControl.getMetadata().getName() == oInfo.type
&& (oInfo.patching
|| (oControl.getFocusDomRef() != oFocusRef
&& (oControlFocusInfo || /*!oControlFocusInfo &&*/ oControl !== oInfo.control || oInfo.preserved)))) {
Log.debug("Apply focus info of control " + oInfo.id, null, "sap.ui.core.FocusHandler");
oInfo.control = oControl;
this.oLastFocusedControlInfo = oInfo;
// Do not store dom patch info in the last focused control info
delete this.oLastFocusedControlInfo.patching;
// expose focus info into the oEventData which is forwarded to the focus info extender
oEventData.info = oInfo.info;
oFocusInfoEventProvider.fireEvent(FOCUS_INFO_EVENT, {
domRef: oControl.getDomRef()
});
oControl.applyFocusInfo(oEventData.info);
// oEventData is given to the event handler as event data, thus we can't assign it with a new empty
// object. We need to clear it by deleting all of its own properties
Object.keys(oEventData).forEach(function(sKey) {
delete oEventData[sKey];
});
} else {
Log.debug("Apply focus info of control " + oInfo.id + " not possible", null, "sap.ui.core.FocusHandler");
}
};
/**
* Destroy method of the Focus Handler.
* It unregisters the event handlers.
*
* @param {jQuery.Event} event the event that initiated the destruction of the FocusHandler
* @private
*/
FocusHandler.prototype.destroy = function(event) {
var oRootRef = event.data.oRootRef;
if (oRootRef) {
oRootRef.removeEventListener("focus", this.fnEventHandler, true);
oRootRef.removeEventListener("blur", this.fnEventHandler, true);
}
};
/**
* Handles the focus/blur events.
*
* @param {FocusEvent} oBrowserEvent Native browser focus/blur event object
* @private
*/
FocusHandler.prototype.onEvent = function(oBrowserEvent){
var oEvent = jQuery.event.fix(oBrowserEvent);
Log.debug("Event " + oEvent.type + " reached Focus Handler (target: " + oEvent.target + (oEvent.target ? oEvent.target.id : "") + ")", null, "sap.ui.core.FocusHandler");
var type = (oEvent.type == "focus" || oEvent.type == "focusin") ? "focus" : "blur";
this.aEventQueue.push({type:type, controlId: getControlIdForDOM(oEvent.target)});
if (this.aEventQueue.length == 1) {
this.processEvent();
}
};
/**
* Processes the focus/blur events in the event queue.
*
* @private
*/
FocusHandler.prototype.processEvent = function(){
var oEvent = this.aEventQueue[0];
if (!oEvent) {
return;
}
try {
if (oEvent.type == "focus") {
this.onfocusEvent(oEvent.controlId);
} else if (oEvent.type == "blur") {
this.onblurEvent(oEvent.controlId);
}
} finally { //Ensure that queue is processed until it is empty!
this.aEventQueue.shift();
if (this.aEventQueue.length > 0) {
this.processEvent();
}
}
};
/**
* Processes the focus event taken from the event queue.
*
* @param {string} sControlId Id of the event related control
* @private
*/
FocusHandler.prototype.onfocusEvent = function(sControlId){
var oControl = getControlById(sControlId);
if (oControl) {
this.oLastFocusedControlInfo = this.getControlFocusInfo(sControlId);
Log.debug("Store focus info of control " + sControlId, null, "sap.ui.core.FocusHandler");
}
this.oCurrent = sControlId;
if (!this.oLast) {
// No last active element to be left...
return;
}
if (this.oLast != this.oCurrent) {
// if same control is focused again (e.g. while re-rendering) no focusleave is needed
triggerFocusleave(this.oLast, sControlId);
}
this.oLast = null;
};
/**
* Processes the blur event taken from the event queue.
*
* @param {string} sControlId Id of the event related control
* @private
*/
FocusHandler.prototype.onblurEvent = function(sControlId){
if (!this.oCurrent) {
// No current Item, so nothing to lose focus...
return;
}
this.oLast = sControlId;
this.oCurrent = null;
setTimeout(this["checkForLostFocus"].bind(this), 0);
};
/**
* Checks for lost focus and provides events in case of losing the focus.
* Called in delayed manner from {@link sap.ui.core.FocusHandler#onblurEvent}.
*
* @private
*/
FocusHandler.prototype.checkForLostFocus = function(){
if (this.oCurrent == null && this.oLast != null) {
triggerFocusleave(this.oLast, null);
}
this.oLast = null;
};
/**
* Tracks the focus before it is lost during DOM preserving.
* Called by the RenderManager when a DOM element is moved to the preserved area.
*
* If the preserved Element contains the activeElement, the focus is set to the body.
*
* In case the currently activeElement is also the last known focus-ref, we need to track
* this information, so the Focus can correctly restored later on.
*
* @param {Element} oCandidate the DOM element that will be preserved
* @private
* @ui5-restricted sap.ui.core.RenderManager
*/
FocusHandler.prototype.trackFocusForPreservedElement = function(oCandidate) {
if (oCandidate.contains(document.activeElement) &&
this.oLastFocusedControlInfo && document.activeElement === this.oLastFocusedControlInfo.focusref) {
// the 'preserved' flag will be read during restoreFocus
this.oLastFocusedControlInfo.preserved = true;
}
};
//***********************************************************
// Utility / convenience
//***********************************************************
/**
* Returns the ID of the control/element to which the given DOM
* reference belongs to or <code>null</code> if no such
* control/element exists.
*
* @param {Element} oDOM the DOM reference
* @returns {string|null} ID of the control or <code>null</code>
* @private
*/
var getControlIdForDOM = function(oDOM){
var sId = jQuery(oDOM).closest("[data-sap-ui]").attr("id");
if (sId) {
return sId;
}
return null;
};
/**
* Calls the onsapfocusleave function on the control with id sControlId
* with the information about the given related control.
*
* @param {string} sControlId
* @param {string} sRelatedControlId
* @private
*/
var triggerFocusleave = function(sControlId, sRelatedControlId){
var oControl = getControlById(sControlId);
if (oControl) {
var oEvent = jQuery.Event("sapfocusleave");
oEvent.target = oControl.getDomRef();
var oRelatedControl = getControlById(sRelatedControlId);
oEvent.relatedControlId = oRelatedControl ? oRelatedControl.getId() : null;
oEvent.relatedControlFocusInfo = oRelatedControl ? oRelatedControl.getFocusInfo() : null;
// TODO: Re-check how focus handling works together with the Popup and different UIAreas
// soft dependency to UIArea to prevent cyclic dependencies (FocusHandler -> UIArea -> FocusHandler)
UIArea = UIArea || sap.ui.require("sap/ui/core/UIArea");
if (UIArea) {
var oControlUIArea = oControl.getUIArea();
var oUIArea = null;
if (oControlUIArea) {
oUIArea = oControlUIArea;
} else {
StaticArea = StaticArea || sap.ui.require("sap/ui/core/StaticArea");
if (StaticArea) {
var oPopupUIAreaDomRef = StaticArea.getDomRef();
if (oPopupUIAreaDomRef.contains(oEvent.target)) {
oUIArea = StaticArea.getUIArea();
}
}
}
if (oUIArea) {
oUIArea._handleEvent(oEvent);
}
}
}
};
function getControlById(sControlId) {
var oControl;
if (!Element) {
Element = sap.ui.require("sap/ui/core/Element");
}
if (Element) {
oControl = Element.getElementById(sControlId);
}
return oControl || null;
}
return new FocusHandler();
});