@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
364 lines (320 loc) • 12.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides utility class sap.ui.core.BlockLayerUtils
sap.ui.define([
'sap/ui/events/jquery/EventTriggerHook',
"sap/base/Log",
"sap/ui/core/Lib",
"sap/ui/thirdparty/jquery"
], function(EventTriggerHook, Log, Library, jQuery) {
"use strict";
/**
* @alias sap.ui.core.BlockLayerUtils
* @static
* @private
* @ui5-restricted sap.ui.core.Control
*/
var BlockLayerUtils = {},
aPreventedEvents = ["focusin", "focusout", "keydown", "keypress", "keyup", "mousedown", "touchstart", "touchmove", "mouseup", "touchend", "click"],
rForbiddenTags = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|tr)$/i;
/**
* Creates a block-state for the given Control and its defined section.
* The returned block-state object contains all jQuery objects related to the newly added blocking layer.
*
* Example:
* oBlockState: {
* $parent: {...}, // The dom ref of the given control,
* $blockLayer: {...} // The dom ref of the added block-layer
* }
*
* Additional all event-handlers (e.g. Tab, Focus,...) are scoped to the block-state.
* When unblock() is called all contained elements/event-handlers will be cleaned-up completely.
*
* @param {sap.ui.core.Control} oControl The specified control to block
* @param {string} sBlockedLayerId The block layer ID
* @param {string} sBlockedSection The block section ID
* @returns {object|undefined} The block-state object containing the parent and block layer DOM or <code>undefined</code> if no valid control instance is provided.
*
* @static
* @private
*/
BlockLayerUtils.block = function(oControl, sBlockedLayerId, sBlockedSection) {
var oParentDomRef, sTag, oBlockState, oBlockLayerDOM;
if (oControl) {
// Retrieves a nested dom ref, but only if the sBlockSection is correctly prefixed with the control-id (best practice)
oParentDomRef = oControl.getDomRef(sBlockedSection);
// Fallback if nested dom ref could not be retrieved
if (!oParentDomRef) {
oParentDomRef = oControl.getDomRef();
}
// if no blocked section/control DOM could be retrieved -> the control is not part of the dom anymore
// this might happen in certain scenarios when e.g. a dialog is closed faster than the busyIndicatorDelay
if (!oParentDomRef) {
Log.warning("BlockLayer could not be rendered. The outer Control instance is not valid anymore or was not rendered yet.");
return;
}
// Check if DOM Element where the busy indicator is supposed to be placed can handle content
sTag = oParentDomRef.tagName;
if (rForbiddenTags.test(sTag)) {
Log.warning("BusyIndicator cannot be placed in elements with tag '" + sTag + "'.");
return;
}
// the block-state contains all relevant DOM elements for the blocked UI sections
oBlockLayerDOM = fnAddHTML(oParentDomRef, sBlockedLayerId);
oBlockState = {
$parent: jQuery(oParentDomRef),
$blockLayer: jQuery(oBlockLayerDOM),
control: oControl
};
//check if the control has static position, if this is the case we need to change it,
//because we relay on relative/absolute/fixed positioning
if (oBlockState.$parent.css('position') == 'static') {
if (oParentDomRef.style && oParentDomRef.style.position === "static") {
oBlockState.originalPosition = 'static';
}
oBlockState.$parent.css('position', 'relative');
oBlockState.positionChanged = true;
}
fnHandleInteraction.call(oBlockState, true);
} else {
Log.warning("BlockLayer couldn't be created. No Control instance given.");
}
return oBlockState;
};
/**
* Removes the block-state
* @param {object} oBlockState The block-state to be removed
* @static
* @private
*/
BlockLayerUtils.unblock = function(oBlockState) {
if (oBlockState) {
// reset the position css attribute to its original value (only used for the value "static")
if (oBlockState.originalPosition) {
oBlockState.$parent.css('position', oBlockState.originalPosition);
} else if (oBlockState.positionChanged) {
// reset the position set to 'relative' by the BlockLayer itself
oBlockState.$parent.css('position', "");
}
// deregister handlers and :before and :after tabbable spans
fnHandleInteraction.call(oBlockState, false);
// restore focus back to control when blocklayer currently has the focus
if (oBlockState.control && oBlockState.$blockLayer[0] && oBlockState.$blockLayer[0].contains(document.activeElement)) {
oBlockState.control.focus();
}
// remove blocklayer from dom
oBlockState.$blockLayer.remove();
}
};
/**
* Adds the necessary ARIA attributes to the given DOM element.
* @param {object} oDOM The DOM element on which the ARIA attributes should be added.
* @private
*/
BlockLayerUtils.addAriaAttributes = function(oDOM) {
var oResourceBundle = Library.get("sap.ui.core").getResourceBundle();
oDOM.setAttribute("role", "progressbar");
oDOM.setAttribute("aria-valuemin", "0");
oDOM.setAttribute("aria-valuemax", "100");
oDOM.setAttribute("aria-valuetext", oResourceBundle.getText("BUSY_VALUE_TEXT"));
oDOM.setAttribute("alt", "");
oDOM.setAttribute("tabindex", "0");
oDOM.setAttribute("title", oResourceBundle.getText("BUSY_TEXT"));
};
/**
* Toggles busy indicator animation for the shared block layer.
* @param {object} oBlockState The shared block-state on which the animation gets toggled
* @param {boolean} bShow Flag to indicate whether the animation should be shown or hidden
* @private
*/
BlockLayerUtils.toggleAnimationStyle = function(oBlockState, bShow) {
var $BS = jQuery(oBlockState.$blockLayer.get(0));
if (bShow) {
// show busy animation in shared block-layer
// marker class for a standalone block-layer is removed
$BS.removeClass("sapUiHiddenBusyIndicatorAnimation");
$BS.removeClass("sapUiBlockLayerOnly");
} else {
// Hide animation in shared block layer
$BS.addClass("sapUiBlockLayerOnly");
$BS.addClass("sapUiHiddenBusyIndicatorAnimation");
}
};
/**
* Adds the BlockLayer to the given DOM.
*
* @param {object} oBlockSection The DOM to be appended. This can be a DOM section or the DomRef of a control.
* @param {string} sBlockedLayerId The controls id
* @returns {object} oContainer The block layer DOM
*
* @private
*/
function fnAddHTML (oBlockSection, sBlockedLayerId) {
var oContainer = document.createElement("div");
oContainer.id = sBlockedLayerId;
oContainer.className = "sapUiBlockLayer ";
BlockLayerUtils.addAriaAttributes(oContainer);
oBlockSection.appendChild(oContainer);
return oContainer;
}
/**
* Suppress interactions on all DOM elements in the blocked section.
*
* Starting with the fnHandleInteraction call, all following function calls are bound in the context of the block-state object.
* Meaning "this" will always reference the block-state object.
*
* @param {boolean} bEnabled New blocked state
* @private
*/
function fnHandleInteraction (bEnabled) {
if (bEnabled) {
var oParentDOM = this.$parent.get(0);
if (oParentDOM){
// Those two elements handle the tab chain so it is not possible to tab behind the busy section.
this.fnRedirectFocus = redirectFocus.bind(this);
this.oTabbableBefore = createTabbable(this.fnRedirectFocus);
this.oTabbableAfter = createTabbable(this.fnRedirectFocus);
oParentDOM.parentNode.insertBefore(this.oTabbableBefore, oParentDOM);
oParentDOM.parentNode.insertBefore(this.oTabbableAfter, oParentDOM.nextSibling);
this._fnSuppressDefaultAndStopPropagationHandler = suppressDefaultAndStopPropagation.bind(this);
this._aSuppressHandler = registerInteractionHandler.call(this, this._fnSuppressDefaultAndStopPropagationHandler);
} else {
Log.warning("fnHandleInteraction called with bEnabled true, but no DOMRef exists!");
}
} else {
if (this.oTabbableBefore) {
removeTabbable(this.oTabbableBefore, this.fnRedirectFocus);
delete this.oTabbableBefore;
}
if (this.oTabbableAfter) {
removeTabbable(this.oTabbableAfter, this.fnRedirectFocus);
delete this.oTabbableAfter;
}
delete this.fnRedirectFocus;
//trigger handler deregistration needs to be done even if DomRef is already destroyed
deregisterInteractionHandler.call(this, this._fnSuppressDefaultAndStopPropagationHandler);
}
/**
* Handler which suppresses event bubbling for blocked section
*
* @param {object} oEvent The event on the suppressed DOM
* @private
*/
function suppressDefaultAndStopPropagation(oEvent) {
var bTargetIsBlockLayer = oEvent.target === this.$blockLayer.get(0),
oTabbable;
if (bTargetIsBlockLayer && oEvent.type === 'keydown' && oEvent.keyCode === 9) {
// Special handling for "tab" keydown: redirect to next element before or after busy section
Log.debug("Local Busy Indicator Event keydown handled: " + oEvent.type);
oTabbable = oEvent.shiftKey ? this.oTabbableBefore : this.oTabbableAfter;
oTabbable.setAttribute("tabindex", -1);
// ignore execution of focus handler
this.bIgnoreFocus = true;
oTabbable.focus();
this.bIgnoreFocus = false;
oTabbable.setAttribute("tabindex", 0);
oEvent.stopImmediatePropagation();
} else if (bTargetIsBlockLayer && (oEvent.type === 'mousedown' || oEvent.type === 'touchstart')) {
// Do not "preventDefault" to allow to focus busy indicator
Log.debug("Local Busy Indicator click handled on busy area: " + oEvent.target.id);
oEvent.stopImmediatePropagation();
} else {
Log.debug("Local Busy Indicator Event Suppressed: " + oEvent.type);
oEvent.preventDefault();
oEvent.stopImmediatePropagation();
}
}
/**
* Captures and redirects focus before it reaches blocked section (from both sides)
*
* @private
*/
function redirectFocus() {
if (!this.bIgnoreFocus) {
// Redirect focus onto busy indicator (if not already focused)
this.$blockLayer.get(0).focus();
}
}
/**
* Create a tabbable span for the block section of the control with according focus handling.
*
* @param {function} fnRedirectFocus Focus handling function
* @returns {object} The span element's DOM node
* @private
*/
function createTabbable(fnRedirectFocus) {
var oBlockSpan = document.createElement("span");
oBlockSpan.setAttribute("tabindex", 0);
oBlockSpan.classList.add("sapUiBlockLayerTabbable");
oBlockSpan.addEventListener('focusin', fnRedirectFocus);
return oBlockSpan;
}
/**
* Create a tabbable span for the block section of the control with according focus handling.
*
* @param {object} oBlockSpan The span element's DOM node
* @param {function} fnRedirectFocus Focus handling function
* @private
*/
function removeTabbable(oBlockSpan, fnRedirectFocus) {
if (oBlockSpan.parentNode) {
oBlockSpan.parentNode.removeChild(oBlockSpan);
}
oBlockSpan.removeEventListener('focusin', fnRedirectFocus);
}
/**
* Register event handler to suppress event within busy section
*
* @param {function} fnHandler The handler function
* @returns {function[]} The suppress handlers
*/
function registerInteractionHandler(fnHandler) {
var aSuppressHandler = [],
oParentDOM = this.$parent.get(0),
oBlockLayerDOM = this.$blockLayer.get(0);
for (var i = 0; i < aPreventedEvents.length; i++) {
// Add event listeners with "useCapture" settings to suppress events before dispatching/bubbling starts
oParentDOM.addEventListener(aPreventedEvents[i], fnHandler, {
capture: true,
passive: false
});
aSuppressHandler.push(EventTriggerHook.suppress(aPreventedEvents[i], oParentDOM, oBlockLayerDOM));
}
//for jQuery triggered events we also need the keydown handler
this.$blockLayer.on('keydown', fnHandler);
return aSuppressHandler;
}
/**
* Deregister event handler to suppress event within busy section
*
* @param {function} fnHandler The handler function
*/
function deregisterInteractionHandler(fnHandler) {
var i,
oParentDOM = this.$parent.get(0),
oBlockLayerDOM = this.$blockLayer.get(0);
if (oParentDOM) {
for (i = 0; i < aPreventedEvents.length; i++) {
// Remove event listeners with "useCapture" settings
oParentDOM.removeEventListener(aPreventedEvents[i], fnHandler, {
capture: true,
passive: false
});
}
}
if (this._aSuppressHandler) {
for (i = 0; i < this._aSuppressHandler.length; i++) {
// this part should be done even no DOMRef exists
EventTriggerHook.release(this._aSuppressHandler[i]);
}
}
if (oBlockLayerDOM) {
this.$blockLayer.off('keydown', fnHandler);
}
}
}
return BlockLayerUtils;
});