@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,507 lines (1,333 loc) • 52.2 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 class sap.ui.core.UIArea
sap.ui.define([
'sap/ui/base/ManagedObject',
'sap/ui/base/ManagedObjectRegistry',
'./Configuration',
'./Element',
'./RenderManager',
'./FocusHandler',
'sap/ui/performance/trace/Interaction',
"sap/ui/util/ActivityDetection",
"sap/ui/events/KeyCodes",
"sap/base/Log",
"sap/base/assert",
"sap/ui/performance/Measurement",
"sap/base/util/uid",
"sap/base/util/isEmptyObject",
"sap/ui/core/Rendering",
'sap/ui/events/jquery/EventExtension',
"sap/ui/events/ControlEvents",
"sap/ui/events/F6Navigation",
"sap/ui/dom/_ready",
"sap/ui/thirdparty/jquery"
],
function(
ManagedObject,
ManagedObjectRegistry,
Configuration,
Element,
RenderManager,
FocusHandler,
Interaction,
ActivityDetection,
KeyCodes,
Log,
assert,
Measurement,
uid,
isEmptyObject,
Rendering,
EventExtension,
ControlEvents,
F6Navigation,
_ready,
jQuery
) {
"use strict";
// Id of the static UIArea
var STATIC_UIAREA_ID = "sap-ui-static";
var oRenderLog = Rendering.getLogger();
// create the RenderManager so it can be used already
var oRenderManager = new RenderManager();
var oCore;
/**
* Whether the DOM is ready (document.ready)
* @private
*/
var bDomReady = false;
_ready().then(function() {
bDomReady = true;
});
EventExtension.apply();
// Activate F6Navigation
jQuery(document).on("keydown", function(oEvent) {
F6Navigation.handleF6GroupNavigation(oEvent, null);
});
var fnDbgWrap = function(oControl) {
return oControl;
},
fnDbgReport = function() {},
fnDbgAnalyzeDelta = function() {};
if ( oRenderLog.isLoggable() ) {
// TODO this supportability feature could be moved out of the standard runtime code and only be loaded on demand
/**
* Records the stack trace that triggered the first invalidation of the given control
*
* @private
*/
fnDbgWrap = function(oControl) {
var location;
try {
throw new Error();
} catch (e) {
location = e.stack || e.stacktrace || (e.sourceURL ? e.sourceURL + ":" + e.line : null);
location = location ? location.split(/\n\s*/g).slice(2) : undefined;
}
return {
obj : oControl,
location : location
};
};
/**
* Creates a condensed view of the controls for which a rendering task is pending.
* Checking the output of this method should help to understand infinite or unexpected rendering loops.
* @private
*/
fnDbgReport = function(that, mControls) {
var mReport = {},
n, oControl;
for (n in mControls) {
// resolve oControl anew as it might have changed
oControl = Element.registry.get(n);
/*eslint-disable no-nested-ternary */
mReport[n] = {
type: oControl ? oControl.getMetadata().getName() : (mControls[n].obj === that ? "UIArea" : "(no such control)"),
location: mControls[n].location,
reason : mControls[n].reason
};
/*eslint-enable no-nested-ternary */
}
oRenderLog.debug(" UIArea '" + that.getId() + "', pending updates: " + JSON.stringify(mReport, null, "\t"));
};
/**
* Creates a condensed view of the controls that have been invalidated but not handled during rendering
* Checking the output of this method should help to understand infinite or unexpected rendering loops.
* @private
*/
fnDbgAnalyzeDelta = function(mBefore, mAfter) {
var n;
for (n in mAfter) {
if ( mBefore[n] != null ) {
if ( mBefore[n].obj !== mAfter[n].obj ) {
mAfter[n].reason = "replaced during rendering";
} else {
mAfter[n].reason = "invalidated again during rendering";
}
} else {
mAfter[n].reason = "invalidated during rendering";
}
}
};
}
/**
* @class An area in a page that hosts a tree of UI elements.
*
* <code>UIArea</code>s are fully managed by the UI5 {@link sap.ui.core.Core Core}. They cannot be created
* by the application but are implicitly created by the Core when controls are placed via
* {@link sap.ui.core.Control#placeAt Control#placeAt} at a new DOM element for which no <code>UIArea</code>
* exists yet.
*
* <code>UIArea</code>s are essential for the rendering of controls. Controls get rendered only when they are
* directly or indirectly contained in the <code>content</code> aggregation of a <code>UIArea</code>.
* <code>Control#placeAt</code> ensures that there is a <code>UIArea</code> with the given ID and adds
* the control to the <code>content</code> aggregation of this <code>UIArea</code>. Whenever controls become
* invalidated, the corresponding <code>UIArea</code> remembers this and takes care of the re-rendering of
* the control.
*
* Additionally, <code>UIArea</code>s play an important role in the event handling of controls. They register for
* a standard set of browser events. For each incoming event, they identify the control to which the target of
* the event belongs to and dispatch the event to that control. This dispatching reduces the number of event
* handlers in a page.
*
* <code>UIArea</code>s also act as a data binding root for their contained controls. Whenever a model is attached
* to or detached from the Core, this change is propagated to all <code>UIAreas</code> which in turn propagate
* it further down to their aggregated children, etc.
*
* The special aggregation named <code>dependents</code> also participates in the databinding, but its content
* is not rendered by the <code>UIArea</code>. It can be used for popups or similar controls that are not contained
* in the normal control tree, but nevertheless should receive model or binding context updates.
*
* @extends sap.ui.base.ManagedObject
* @author SAP SE
* @version 1.111.5
* @param {object} [oRootNode] reference to the DOM element that should be 'hosting' the UI Area.
* @public
* @alias sap.ui.core.UIArea
* @hideconstructor
*/
var UIArea = ManagedObject.extend("sap.ui.core.UIArea", {
constructor: function(oRootNode) {
if (arguments.length === 0) {
return;
}
// Note: UIArea has a modifiable Id. This doesn't perfectly match the default behavior of ManagedObject
// But UIArea overrides getId().
ManagedObject.apply(this);
this.bLocked = false;
this.bInitial = true;
this.aContentToRemove = [];
this.bNeedsRerendering = false;
if (oRootNode != null) {
this._setRootNode(oRootNode);
// Figure out whether UI Area is pre-rendered (server-side JS rendering)!
this.bNeedsRerendering = this.bNeedsRerendering && !document.getElementById(oRootNode.id + "-Init");
}
this.mInvalidatedControls = {};
if (!this.bNeedsRerendering) {
this.bRenderSelf = false;
} else {
// Rendering needs to be notified about an invalid UIArea
Rendering.invalidateUIArea(this);
}
},
metadata: {
// ---- object ----
publicMethods : ["setRootNode", "getRootNode", "setRootControl", "getRootControl", "lock","unlock", "isLocked"],
aggregations : {
/**
* Content that is displayed in the UIArea.
*/
content : {type : "sap.ui.core.Control", multiple : true, singularName : "content"},
/**
* Dependent objects whose lifecycle is bound to the UIArea but which are not automatically rendered by the UIArea.
*/
dependents : {type : "sap.ui.core.Control", multiple : true}
}
},
// make 'dependents' a non-invalidating aggregation
insertDependent: function(oElement, iIndex) {
return this.insertAggregation("dependents", oElement, iIndex, true);
},
addDependent: function(oElement) {
return this.addAggregation("dependents", oElement, true);
},
removeDependent: function(vElement) {
return this.removeAggregation("dependents", vElement, true);
},
removeAllDependents: function() {
return this.removeAllAggregation("dependents", true);
},
destroyDependents: function() {
return this.destroyAggregation("dependents", true);
}
});
/**
* Returns whether re-rendering is currently suppressed on this UIArea.
*
* @returns {boolean} Whether re-rendering is currently suppressed on this UIArea
* @protected
*/
UIArea.prototype.isInvalidateSuppressed = function() {
return this.iSuppressInvalidate > 0;
};
/**
* Returns this <code>UIArea</code>'s id (as determined from provided RootNode).
* @return {string|null} id of this UIArea
* @public
*/
UIArea.prototype.getId = function() {
return this.oRootNode ? this.oRootNode.id : null;
};
/**
* Returns this UI area. Needed to stop recursive calls from an element to its parent.
*
* @return {sap.ui.core.UIArea} this
* @protected
*/
UIArea.prototype.getUIArea = function() {
return this;
};
/**
* Allows setting the root node hosting this instance of <code>UIArea</code>.
*
* The node must have an ID that will be used as ID for this instance of <code>UIArea</code>.
*
* @param {Element} oRootNode
* the hosting DOM node for this instance of <code>UIArea</code>.
* @public
* @deprecated as of version 1.107.0
*/
UIArea.prototype.setRootNode = function(oRootNode) {
this._setRootNode(oRootNode);
};
/**
* Allows setting the root node hosting this instance of <code>UIArea</code>.
*
* The node must have an ID that will be used as ID for this instance of <code>UIArea</code>.
*
* @param {Element} oRootNode
* the hosting DOM node for this instance of <code>UIArea</code>.
* @private
*/
UIArea.prototype._setRootNode = function(oRootNode) {
if (this.oRootNode === oRootNode) {
return;
}
// oRootNode must either be empty or must be a DOMElement and must not be root node of some other UIArea
assert(!oRootNode || (oRootNode.nodeType === 1 && !jQuery(oRootNode).attr("data-sap-ui-area")), "UIArea root node must be a DOMElement");
//TODO IS there something missing
if (this.oRootNode) {
this._ondetach();
}
// UIArea gets its id from the rootNode, so we must update the registry
this.deregister();
this.oRootNode = oRootNode;
this.register();
if ( this.getContent().length > 0 ) {
this.invalidate();
}
if (this.oRootNode) {
// prepare eventing
this._onattach();
}
};
/**
* Returns the Root Node hosting this instance of <code>UIArea</code>.
*
* @return {Element} the Root Node hosting this instance of <code>UIArea</code>.
* @public
*/
UIArea.prototype.getRootNode = function() {
return this.oRootNode;
};
/**
* Sets the root control to be displayed in this UIArea.
*
* First, all old content controls (if any) will be detached from this UIArea (e.g. their parent
* relationship to this UIArea will be cut off). Then the parent relationship for the new
* content control (if not empty) will be set to this UIArea and finally, the UIArea will
* be marked for re-rendering.
*
* The real re-rendering happens whenever the re-rendering is called. Either implicitly
* at the end of any control event or by calling sap.ui.getCore().applyChanges().
*
* @param {sap.ui.base.Interface | sap.ui.core.Control} oRootControl
* the Control that should be the Root for this <code>UIArea</code>.
* @public
* @deprecated As of version 1.1, use {@link #removeAllContent} and {@link #addContent} instead
*/
UIArea.prototype.setRootControl = function(oRootControl) {
this.removeAllContent();
this.addContent(oRootControl);
};
/**
* Returns the content control of this <code>UIArea</code> at the specified index.
* If no index is given the first content control is returned.
*
* @param {int} idx index of the control in the content of this <code>UIArea</code>
* @return {sap.ui.core.Control} the content control of this <code>UIArea</code> at the specified index.
* @public
* @deprecated As of version 1.1, use function {@link #getContent} instead
*/
UIArea.prototype.getRootControl = function(idx) {
var aContent = this.getContent();
if (aContent.length > 0) {
if (idx >= 0 && idx < aContent.length) {
return aContent[idx];
}
return aContent[0];
}
return null;
};
UIArea.prototype._addRemovedContent = function(oDomRef) {
if (this.oRootNode && oDomRef) {
this.aContentToRemove.push(oDomRef);
}
};
/*
* See generated JSDoc
*/
UIArea.prototype.addContent = function(oContent, _bSuppressInvalidate) {
this.addAggregation("content", oContent, _bSuppressInvalidate);
// TODO this remains here just to make the UX3 Shell work which doesn't invalidate properly
if ( _bSuppressInvalidate !== true ) {
this.invalidate();
}
return this;
};
/*
* See generated JSDoc
*/
UIArea.prototype.removeContent = function(vContent, /* internal only */ _bSuppressInvalidate) {
var oContent = this.removeAggregation("content", vContent, _bSuppressInvalidate);
if ( !_bSuppressInvalidate ) {
var oDomRef;
if (oContent && oContent.getDomRef) {
oDomRef = oContent.getDomRef();
}
this._addRemovedContent(oDomRef);
//this.invalidate();
}
return oContent;
};
/*
* See generated JSDoc
*/
UIArea.prototype.removeAllContent = function() {
var aContent = this.removeAllAggregation("content");
for (var idx = 0; idx < aContent.length; idx++) {
var oDomRef;
var oContent = aContent[idx];
if (oContent && oContent.getDomRef) {
oDomRef = oContent.getDomRef();
}
this._addRemovedContent(oDomRef);
}
//this.invalidate();
return aContent;
};
/*
* See generated JSDoc
*/
UIArea.prototype.destroyContent = function() {
var aContent = this.getContent();
for (var idx = 0; idx < aContent.length; idx++) {
var oDomRef;
var oContent = aContent[idx];
if (oContent && oContent.getDomRef) {
oDomRef = oContent.getDomRef();
}
this._addRemovedContent(oDomRef);
}
this.destroyAggregation("content");
//this.invalidate();
return this;
};
/**
* Locks this instance of UIArea.
*
* Rerendering and eventing will not be active as long as no
* {@link #unlock} is called.
*
* @public
*/
UIArea.prototype.lock = function() {
this.bLocked = true;
};
/**
* Un-Locks this instance of UIArea.
*
* Rerendering and eventing will now be enabled again.
*
* @public
*/
UIArea.prototype.unlock = function() {
if ( this.bLocked && this.bNeedsRerendering ) {
// While being locked, we might have ignored a call to rerender()
// Therefore notify the Rendering (again)
Rendering.invalidateUIArea(this);
}
this.bLocked = false;
};
/**
* Returns the locked state of the <code>sap.ui.core.UIArea</code>
* @return {boolean} locked state
* @public
*/
UIArea.prototype.isLocked = function () {
return this.bLocked;
};
/**
* Provide getBindingContext, as UIArea can be parent of an element.
*
* @returns {null} Always returns null.
*
* @protected
*/
UIArea.prototype.getBindingContext = function(){
return null;
};
/**
* Returns the Core's event provider as new eventing parent to enable control event bubbling to the core
* to ensure compatibility with the core validation events.
*
* @return {sap.ui.base.EventProvider} the parent event provider
* @protected
*/
UIArea.prototype.getEventingParent = function() {
return oCore ? oCore._getEventProvider() : undefined;
};
// ###########################################################################
// Convenience for methods
// e.g. Process Events for inner Controls
// or figure out whether control is part of this area.
// ###########################################################################
/**
* Checks whether the control is still valid (is in the DOM)
*
* @return {boolean} True if the control is still in the active DOM
* @protected
*/
UIArea.prototype.isActive = function() {
return !!this.getId() && document.getElementById(this.getId()) != null;
};
/**
* Triggers asynchronous re-rendering of the <code>UIArea</code>'s content.
*
* Serves as an end-point for the bubbling of invalidation requests along the
* element/control aggregation hierarchy.
*
* @protected
*/
UIArea.prototype.invalidate = function() {
this.addInvalidatedControl(this);
};
/**
* Notifies the <code>UIArea</code> about an invalidated descendant control.
*
* During re-rendering, the <code>UIArea</code> internally decides whether to re-render just the modified
* controls or the complete content. It also informs the <code>Core</code> when it becomes invalid
* for the first time.
*
* @param {object} oControl Descendant control that got invalidated
* @private
*/
UIArea.prototype.addInvalidatedControl = function(oControl){
// if UIArea is already marked for a full rendering, there is no need to record invalidated controls
if ( this.bRenderSelf ) {
return;
}
// inform the Rendering, if we are getting invalid now
if ( !this.bNeedsRerendering ) {
Rendering.invalidateUIArea(this);
}
var sId = oControl.getId();
//check whether the control is already invalidated
if ( oControl === this ) {
this.bRenderSelf = true; //everything in this UIArea
this.bNeedsRerendering = true;
this.mInvalidatedControls = {};
this.mInvalidatedControls[sId] = fnDbgWrap(this);
return;
}
if (this.mInvalidatedControls[sId]) {
return;
}
if (!this.bRenderSelf) {
//add it to the list of controls
this.mInvalidatedControls[sId] = fnDbgWrap(oControl);
this.bNeedsRerendering = true;
}
};
/**
* Synchronously renders any pending UI updates.
*
* Either renders the whole <code>UIArea</code> or a set of descendant controls that have been invalidated.
*
* @param {boolean} bForce Whether a re-rendering of the <code>UIArea</code> should be enforced
* @return {boolean} Whether a redraw was necessary or not
* @private
*/
UIArea.prototype.rerender = function(bForce) {
var that = this;
function clearRenderingInfo() {
that.bRenderSelf = false;
that.aContentToRemove = [];
that.mInvalidatedControls = {};
that.bNeedsRerendering = false;
}
if (bForce) {
this.bNeedsRerendering = true;
}
if ( this.bLocked || !this.bNeedsRerendering ) {
return false;
}
// Keep a reference to the collected rendering info and attach a new, empty info to this instance.
// Any concurrent modification will be collected as new info and trigger a new automated rendering
var bRenderSelf = this.bRenderSelf,
aContentToRemove = this.aContentToRemove,
mInvalidatedControls = this.mInvalidatedControls,
bUpdated = false;
clearRenderingInfo();
// pause performance measurement for all UI Areas
Measurement.pause("renderPendingUIUpdates");
// start performance measurement
Measurement.start(this.getId() + "---rerender","Rerendering of " + this.getMetadata().getName());
fnDbgReport(this, mInvalidatedControls);
if (bRenderSelf) { // full UIArea rendering
if (this.oRootNode) {
oRenderLog.debug("Full Rendering of UIArea '" + this.getId() + "'");
// save old content
RenderManager.preserveContent(this.oRootNode, /* bPreserveRoot */ false, /* bPreserveNodesWithId */ this.bInitial);
this.bInitial = false;
var cleanUpDom = function(aCtnt, bCtrls){
var len = aCtnt.length;
var oDomRef;
for (var i = 0; i < len; i++) {
oDomRef = bCtrls ? aCtnt[i].getDomRef() : aCtnt[i];
if ( oDomRef && !RenderManager.isPreservedContent(oDomRef) && that.oRootNode === oDomRef.parentNode) {
jQuery(oDomRef).remove();
}
}
return len;
};
var oFocusRef_Initial = document.activeElement;
var oStoredFocusInfo = FocusHandler.getControlFocusInfo();
//First remove the old Dom nodes and then render the controls again
cleanUpDom(aContentToRemove);
var aContent = this.getContent();
var len = cleanUpDom(aContent, true);
var oFocusRef_AfterCleanup = document.activeElement;
for (var i = 0; i < len; i++) {
if (aContent[i] && aContent[i].getParent() === this) {
oRenderManager.render(aContent[i], this.oRootNode, true);
}
}
bUpdated = true;
/* Try restoring focus when focus ref is changed due to cleanup operations and not changed anymore by the rendering logic */
if (oFocusRef_Initial && oFocusRef_Initial != oFocusRef_AfterCleanup && oFocusRef_AfterCleanup === document.activeElement) {
try {
FocusHandler.restoreFocus(oStoredFocusInfo);
} catch (e) {
Log.warning("Problems while restoring the focus after full UIArea rendering: " + e, null, this);
}
}
} else {
// cannot re-render now; wait!
oRenderLog.debug("Full Rendering of UIArea '" + this.getId() + "' postponed, no root node");
}
} else { // only partial update (invalidated controls)
var isRenderedTogetherWithAncestor = function(oCandidate) {
for (;;) {
// Controls that implement marker interface sap.ui.core.PopupInterface are by contract not rendered by their parent.
// Therefore the search for to-be-rendered ancestors must be stopped when such a control is reached.
if ( oCandidate.getMetadata && oCandidate.getMetadata().isInstanceOf("sap.ui.core.PopupInterface") ) {
break;
}
oCandidate = oCandidate.getParent();
// If the candidate is null/undefined or the UIArea itself
// they do-while loop will be interrupted
if ( !oCandidate || oCandidate === that ) {
return false;
}
// If the candidate is listed in the invalidated controls map
// it will be re-rendered together with the UIArea. Inline
// templates are a special case because they share their ID
// with the UIArea and therefore the detection will ignore
// the inline templates since they should be re-rendered with
// their UIArea.
if ( mInvalidatedControls.hasOwnProperty(oCandidate.getId()) ) {
return true;
}
}
};
var aControlsRenderedTogetherWithAncestor = [];
for (var n in mInvalidatedControls) {
var oControl = Element.registry.get(n);
// CSN 0000834961 2011: control may have been destroyed since invalidation happened -> check whether it still exists
if ( oControl ) {
if ( !isRenderedTogetherWithAncestor(oControl) ) {
oControl.rerender();
bUpdated = true;
} else {
aControlsRenderedTogetherWithAncestor.push(oControl);
}
}
}
/**
* Let us suppose that A is the parent of B, and B is the parent of C. The controls A and C are invalidated, but B isn't.
* Controls A and C will be added to the UIArea as invalidated controls. At the next tick, UIArea will be rendered again.
* Thanks to the isRenderedTogetherWithAncestor method above, C.rerender will never be executed but only A.rerender.
*
* In apiVersion 1 or 2:
* During the rendering of A, RM.renderControl(B) renders the control B, and during the rendering of B, RM.renderControl(C)
* renders the control C. At the end of the UIArea re-rendering, there shall be no control remaining in an invalidated state.
*
* In apiVersion 4:
* During the rendering of A when RM.renderControl(B) is called the RenderManager first checks whether control B is
* invalidated. Since it was not invalidated the RenderManager skips the rendering of control B. Consequently, there will be
* no RM.renderControl(C) call to render the control C, and it remains in an invalidated state.
*
* The implementation below re-renders the invalidated controls that are skipped and not rendered with their ancestor.
* The re-rendering here is only required for controls that already have DOM output.
*/
aControlsRenderedTogetherWithAncestor.forEach(function(oControl) {
if (!oControl._bNeedsRendering) {
return;
}
if (oControl.bOutput == true && oControl.getDomRef() ||
oControl.bOutput == "invisible" && document.getElementById(RenderManager.createInvisiblePlaceholderId(oControl))) {
oControl.rerender();
}
});
}
// enrich the bookkeeping
fnDbgAnalyzeDelta(mInvalidatedControls, this.mInvalidatedControls);
// uncomment the following line for old behavior:
// clearRenderingInfo();
// end performance measurement
Measurement.end(this.getId() + "---rerender");
// resume performance measurement for all UI Areas
Measurement.resume("renderPendingUIUpdates");
return bUpdated;
};
/**
* Receives a notification from the RenderManager immediately after a control has been rendered.
*
* Only at that moment, registered invalidations are obsolete. If they happen (again) after
* that point in time, the previous rendering cannot reflect the changes that led to the
* invalidation and therefore a new rendering is required.
*
* Therefore, pending invalidations can only be cleared at this point in time.
* @private
*/
UIArea.prototype._onControlRendered = function(oControl) {
var sId = oControl.getId();
if ( this.mInvalidatedControls[sId] ) {
delete this.mInvalidatedControls[sId];
}
};
/**
* Rerenders the given control
* @see sap.ui.core.Control.rerender()
* @param oControl
* @private
*/
UIArea.rerenderControl = function(oControl){
var oDomRef = null;
if (oControl) {
oDomRef = oControl.getDomRef();
if (!oDomRef || RenderManager.isPreservedContent(oDomRef) ) {
// In case no old DOM node was found or only preserved DOM, search for an 'invisible' placeholder
oDomRef = document.getElementById(RenderManager.RenderPrefixes.Invisible + oControl.getId());
}
}
var oParentDomRef = oDomRef && oDomRef.parentNode; // remember parent here as preserveContent() might move the node!
if (oParentDomRef) {
var oUIArea = oControl.getUIArea();
// Why this is really needed?
var oRM = oUIArea ? oRenderManager : new RenderManager();
oRenderLog.debug("Rerender Control '" + oControl.getId() + "'" + (oUIArea ? "" : " (using a temp. RenderManager)"));
RenderManager.preserveContent(oDomRef, /* bPreserveRoot */ true, /* bPreserveNodesWithId */ false, oControl /* oControlBeforeRerender */);
oRM.render(oControl, oParentDomRef);
} else {
var oUIArea = oControl.getUIArea();
oUIArea && oUIArea._onControlRendered(oControl);
oRenderLog.warning("Couldn't rerender '" + oControl.getId() + "', as its DOM location couldn't be determined");
}
};
var rEvents = /^(mousedown|mouseup|click|keydown|keyup|keypress|touchstart|touchend|tap)$/;
var aPreprocessors = [], aPostprocessors = [];
var mVerboseEvents = {mousemove: 1, mouseover: 1, mouseout: 1, scroll: 1, dragover: 1, dragenter: 1, dragleave: 1};
/**
* Adds an event handler that will be executed before the event is dispatched.
* @param {Function} fnPreprocessor The event handler to add
* @private
*/
UIArea.addEventPreprocessor = function(fnPreprocessor) {
aPreprocessors.push(fnPreprocessor);
};
/**
* Gets the event handlers that will be executed before the event is dispatched.
* @return {Function[]} The event preprocessors
* @private
*/
UIArea.getEventPreprocessors = function() {
return aPreprocessors;
};
/**
* Adds an event handler that will be executed after the event is dispatched.
* @param {Function} fnPostprocessor The event handler to add
* @private
*/
UIArea.addEventPostprocessor = function(fnPostprocessor) {
aPostprocessors.push(fnPostprocessor);
};
/**
* Gets the event handlers that will be executed after the event is dispatched.
* @return {Function[]} The event postprocessors
* @private
*/
UIArea.getEventPostprocessors = function() {
return aPostprocessors;
};
/**
* Enabled or disables logging of certain event types.
*
* The event handling code of class UIArea logs all processed browser events with log level DEBUG.
* Only some events that occur too frequently are suppressed by default: <code>mousemove</code>,
* <code>mouseover</code>, <code>mouseout</code>, <code>scroll</code>, <code>dragover</code>,
* <code>dragenter</code> and <code>dragleave</code>.
*
* With this method, logging can be disabled for further event types or it can be enabled for
* some or all of the event types listed above. The parameter <code>mEventTypes</code> is a map
* of boolean values keyed by event type names. When the value for an event type coerces to true,
* events of that type won't be logged.
*
* @example
* sap.ui.require(['sap/ui/core/UIArea'], function(UIArea) {
* UIArea.configureEventLogging({
* mouseout: false, // no longer suppress logging of mouseout events
* focusin: 1 // suppress logging of focusin events
* });
* });
*
* @param {Object<string, boolean>} [mEventTypes] Map of logging flags keyed by event types
* @returns {Object<string, boolean>} A copy of the resulting event logging configuration (not normalized)
* @public
* @since 1.62
*/
UIArea.configureEventLogging = function(mEventTypes) {
Object.assign(mVerboseEvents, mEventTypes);
return Object.assign({}, mVerboseEvents); // return a copy
};
/**
* Handles all incoming DOM events centrally and dispatches the event to the
* registered event handlers.
* @param {jQuery.Event} oEvent the jQuery event object
* @private
* @ui5-restricted sap.ui.core.dnd.DragAndDrop, sap.ui.core.FocusHandler
*/
UIArea.prototype._handleEvent = function(/**event*/oEvent) {
// execute the registered event handlers
var oTargetElement,
oElement,
bInteractionRelevant;
// TODO: this should be the 'lowest' SAPUI5 Control of this very
// UIArea instance's scope -> nesting scenario
oTargetElement = oElement = Element.closestTo(oEvent.target);
ActivityDetection.refresh();
if (oTargetElement == null) {
return;
}
// the mouse event which is fired by mobile browser with a certain delay after touch event should be suppressed
// in event delegation.
if (oEvent.isMarked("delayedMouseEvent")) {
return;
}
var sHandledUIAreaId = oEvent.getMark("handledByUIArea"),
sId = this.getId();
//if event is already handled by inner UIArea (as we use the bubbling phase now), returns.
//if capturing phase would be used, here means event is already handled by outer UIArea.
if (sHandledUIAreaId && sHandledUIAreaId !== sId) {
oEvent.setMark("firstUIArea", false);
return;
}
oEvent.setMarked("firstUIArea");
// store the element on the event (aligned with jQuery syntax)
oEvent.srcControl = oTargetElement;
// in case of CRTL+SHIFT+ALT the contextmenu event should not be dispatched
// to allow to display the browsers context menu
if (oEvent.type === "contextmenu" && oEvent.shiftKey && oEvent.altKey && (oEvent.metaKey || oEvent.ctrlKey)) {
Log.info("Suppressed forwarding the contextmenu event as control event because CTRL+SHIFT+ALT is pressed!");
return;
}
aPreprocessors.forEach(function(fnPreprocessor){
fnPreprocessor(oEvent);
});
// forward the control event:
// if the control propagation has been stopped or the default should be
// prevented then do not forward the control event.
if (oCore) {
oCore._handleControlEvent(oEvent, sId);
}
// if the UIArea or the Core is locked then we do not dispatch
// any event to the control => but they will still be dispatched
// as control event afterwards!
if (this.bLocked || (oCore && oCore.isLocked())) {
return;
}
// notify interaction tracing for relevant event - it is important to have evaluated all the previous switches
// in case the method would return before dispatching the event, we should not notify an event start
if (Interaction.getActive()) {
bInteractionRelevant = oEvent.type.match(rEvents);
if (bInteractionRelevant) {
Interaction.notifyEventStart(oEvent);
}
}
// retrieve the pseudo event types
var aEventTypes = [];
if (oEvent.getPseudoTypes) {
aEventTypes = oEvent.getPseudoTypes();
}
aEventTypes.push(oEvent.type);
//enable check for fieldgroup change
var bGroupChanged = false;
// dispatch the event to the controls (callback methods: onXXX)
while (oElement instanceof Element && oElement.isActive() && !oEvent.isPropagationStopped()) {
var sScopeCheckId = oEvent.getMark("scopeCheckId"),
oScopeCheckDOM = sScopeCheckId && window.document.getElementById(sScopeCheckId),
oDomRef = oElement.getDomRef();
// for events which are dependent on the scope DOM (the DOM on which the 'mousedown' event is fired), the
// event is dispatched to the element only when the element's root DOM contains or equals the scope check
// DOM, so that the simulated 'touchmove' and 'touchend' event is only dispatched to the element when the
// 'touchstart' also occurred on the same element
if (!oScopeCheckDOM || (oDomRef && oDomRef.contains(oScopeCheckDOM))) {
// for each event type call the callback method
// if the execution should be stopped immediately
// then no further callback method will be executed
for (var i = 0, is = aEventTypes.length; i < is; i++) {
var sType = aEventTypes[i];
oEvent.type = sType;
// ensure currenTarget is the DomRef of the handling Control
oEvent.currentTarget = oElement.getDomRef();
oElement._handleEvent(oEvent);
if (oEvent.isImmediatePropagationStopped()) {
break;
}
}
if (!bGroupChanged && !oEvent.isMarked("enterKeyConsumedAsContent")) {
bGroupChanged = this._handleGroupChange(oEvent,oElement);
}
// if the propagation is stopped do not bubble up further
if (oEvent.isPropagationStopped()) {
break;
}
// Secret property on the element to allow to cancel bubbling of all events.
// This is a very special case, so there is no API method for this in the control.
if (oElement.bStopEventBubbling) {
break;
}
// This is the (not that common) situation that the element was deleted in its own event handler.
// i.e. the Element became 'inactive' (see Element#isActive())
oDomRef = oElement.getDomRef();
if (!oDomRef) {
break;
}
}
// bubble up to the parent
oDomRef = oDomRef.parentNode;
oElement = null;
// Only process the touchend event which is emulated from mouseout event when the current domRef
// doesn't equal or contain the relatedTarget
if (oEvent.isMarked("fromMouseout") && (oDomRef && oDomRef.contains(oEvent.relatedTarget))) {
break;
}
// ensure we do not bubble the control tree higher than our rootNode
while (oDomRef && oDomRef !== this.getRootNode()) {
if (oDomRef.id) {
oElement = Element.closestTo(oDomRef);
if (oElement) {
break;
}
}
oDomRef = oDomRef.parentNode;
}
}
aPostprocessors.forEach(function(fnPostprocessor){
fnPostprocessor(oEvent);
});
if (bInteractionRelevant) {
Interaction.notifyEventEnd(oEvent);
}
// reset previously changed currentTarget
oEvent.currentTarget = this.getRootNode();
// mark on the event that it's already handled by this UIArea
oEvent.setMark("handledByUIArea", sId);
// TODO: rethink about logging levels!
// logging: propagation stopped
if (oEvent.isPropagationStopped()) {
Log.debug("'" + oEvent.type + "' propagation has been stopped");
}
// logging: prevent the logging of some events that are verbose and for others do some logging into the console
var sEventName = oEvent.type;
if (!mVerboseEvents[sEventName]) {
if (oTargetElement) {
Log.debug("Event fired: '" + sEventName + "' on " + oTargetElement, "", "sap.ui.core.UIArea");
} else {
Log.debug("Event fired: '" + sEventName + "'", "", "sap.ui.core.UIArea");
}
}
};
/*
* The onattach function is called when the Element is attached to the DOM
* @private
*/
UIArea.prototype._onattach = function() {
// TODO optimizations for 'matching event list' could be done here.
// // create the events string (space separated list of event names):
// // the first time a control is attached - it will determine the required
// // events and store this information in the controls metadata which is
// // shared across the control instances.
// if (!this.getMetadata().sEvents) {
//
// // shorten the access to the array of events and pseudo events
// var aEv = ControlEvents.events;
// var oPsEv = PseudoEvents.events; // required from sap/ui/events/PseudoEvents
//
// // create the data structures for the event handler registration
// this.sEvents = "";
// var aEvents = [];
//
// // check for pseudo events and register them for their relevant types
// for (var evt in oPsEv) {
// for (j = 0, js = oPsEv[evt].aTypes.length; j < js; j++) {
// var type = oPsEv[evt].aTypes[j];
// if (aEvents.indexOf(type) === -1) {
// aEvents.push(type);
// }
// }
// }
//
// // check for events and register them
// for (var i = 0, is = aEv.length; i < is; i++) {
// var type = aEv[i];
// if (aEvents.indexOf(type) === -1) {
// aEvents.push(type);
// }
// }
//
// // keep the list of events for the jQuery bind/unbind method
// this.sEvents = aEvents.join(" ");
//
// // cache the event handlers registry map
// this.getMetadata().sEvents = this.sEvents;
//
// } else {
// // use the cached map of event handlers
// this.sEvents = this.getMetadata().sEvents;
// }
// check for existing root node
var oDomRef = this.getRootNode();
if (oDomRef == null) {
return;
}
// mark the DOM as UIArea and bind the required events
jQuery(oDomRef).attr("data-sap-ui-area", oDomRef.id).on(ControlEvents.events.join(" "), this._handleEvent.bind(this));
};
/**
* The ondetach function is called when the Element is detached out of the DOM
* @private
*/
UIArea.prototype._ondetach = function() {
// check for existing root node
var oDomRef = this.getRootNode();
if (oDomRef == null) {
return;
}
// remove UIArea marker and unregister all event handlers of the control
jQuery(oDomRef).removeAttr("data-sap-ui-area").off();
// TODO: when optimizing the events => take care to unbind only the
// required. additionally consider not to remove other event handlers.
// var ojQRef = jQuery(oDomRef);
// if (this.sEvents) {
// ojQRef.off(this.sEvents, this._handleEvent);
// }
//
// ojQRef.off("focus",FocusHandler.onfocusin);
// ojQRef.off("blur", FocusHandler.onfocusout);
};
/**
* UIAreas can't be cloned and throw an error when trying to do so.
*/
UIArea.prototype.clone = function() {
throw new Error("UIArea can't be cloned");
};
/**
* Handles field group change or validation based on the given browser event.
*
* Triggers the <code>changeGroup</code> event (with reason: validate) for current field group control.
*
* @param {jQuery.Event} oEvent Browser event
* @param {sap.ui.core.Element} oElement UI5 <code>Element</code> where the event occurred
*
* @return {boolean} true if the field group control was set or validated.
*
* @private
*/
UIArea.prototype._handleGroupChange = function(oEvent, oElement) {
var oKey = UIArea._oFieldGroupValidationKey;
if (oEvent.type === "focusin" || oEvent.type === "focusout") {
if (oEvent.type === "focusout") {
oElement = Element.closestTo(document.activeElement);
}
// delay the check for a field group change to allow focus forwarding and resetting focus after selection
if (UIArea._iFieldGroupDelayTimer) {
clearTimeout(UIArea._iFieldGroupDelayTimer);
UIArea._iFieldGroupDelayTimer = null;
}
UIArea._iFieldGroupDelayTimer = setTimeout(this.setFieldGroupControl.bind(this, oElement), 0);
return true; //no further checks because setFieldGroupControl already looked for a group id and fired the enter and leave events that bubble
} else if (this.getFieldGroupControl() &&
oEvent.type === "keyup" &&
oEvent.keyCode === oKey.keyCode &&
oEvent.shiftKey === oKey.shiftKey &&
oEvent.altKey === oKey.altKey &&
oEvent.ctrlKey === oKey.ctrlKey) {
// check for field group change (validate) only after events where processed by elements
if (UIArea._iFieldGroupTriggerDelay) {
clearTimeout(UIArea._iFieldGroupTriggerDelay);
}
var oCurrentControl = this.getFieldGroupControl(),
aCurrentGroupIds = (oCurrentControl ? oCurrentControl._getFieldGroupIds() : []);
if (aCurrentGroupIds.length > 0) {
oCurrentControl.triggerValidateFieldGroup(aCurrentGroupIds);
}
return true; //no further checks because setFieldGroupControl already looked for a group id and fired the enter and leave events that bubble
}
return false;
};
/**
* Sets the field group control and triggers the validateFieldGroup event for
* the current field group control.
* There is only one field group control for all UI areas.
*
* @param {sap.ui.core.Element} oElement the new field group control
*
* @return {sap.ui.core.UIArea} the UI area that the active field group control belongs to.
*
* @private
*/
UIArea.prototype.setFieldGroupControl = function(oElement) {
var oControl = oElement;
while ( oControl && !(oControl instanceof Element && oControl.isA("sap.ui.core.Control")) ) {
oControl = oControl.getParent();
}
var oCurrentControl = this.getFieldGroupControl();
if ( oControl != oCurrentControl && document.activeElement && (document.activeElement.id !== "sap-ui-static-firstfe")) {
var aCurrentGroupIds = (oCurrentControl ? oCurrentControl._getFieldGroupIds() : []),
aNewGroupIds = (oControl ? oControl._getFieldGroupIds() : []),
aTargetFieldGroupIds = aCurrentGroupIds.filter(function(sCurrentGroupId) {
return aNewGroupIds.indexOf(sCurrentGroupId) < 0;
});
if (aTargetFieldGroupIds.length > 0) {
oCurrentControl.triggerValidateFieldGroup(aTargetFieldGroupIds);
}
UIArea._oFieldGroupControl = oControl;
}
return this;
};
/**
* Returns the current valid field group control.
*
* There is only one field group control for all UI areas.
*
* @returns {sap.ui.core.Control|null} the current valid field group control or <code>null</code>.
*
* @private
*/
UIArea.prototype.getFieldGroupControl = function() {
if (UIArea._oFieldGroupControl && !UIArea._oFieldGroupControl.bIsDestroyed) {
return UIArea._oFieldGroupControl;
}
return null;
};
// apply the registry mixin
ManagedObjectRegistry.apply(UIArea, {
onDuplicate: function(sId, oldUIArea, newUIArea) {
var sMsg = "adding UIArea with duplicate id '" + sId + "'";
Log.error(sMsg);
throw new Error("Error: " + sMsg);
}
});
// field group static members
/*
* Group control for all UI areas to handle change of field groups
* @private
*/
UIArea._oFieldGroupControl = null;
/*
* delay timer for triggering field group changes if focus is forwarded or temporarily dispatched by selection
* @private
*/
UIArea._iFieldGroupDelayTimer = null;
/*
* Keycode and modifier combination that is used to fire a change group event (reason: validate)
* @private
*/
UIArea._oFieldGroupValidationKey = {
keyCode : KeyCodes.ENTER,
shiftKey : false,
altKey: false,
ctrlKey: false
};
// share the render log with Core
UIArea._oRenderLog = oRenderLog;
/**
* Creates a new {@link sap.ui.core.UIArea UIArea}.
* Must only be used by sap.ui.core.Core or sap.ui.core.Control.
*
* @param {Element|string} oDomRef a DOM Element or ID string of the UIArea
* @return {sap.ui.core.UIArea} a new UIArea
* @private
* @ui5-restricted sap.ui.core
*/
UIArea.create = function(oDomRef) {
assert(typeof oDomRef === "string" || typeof oDomRef === "object", "oDomRef must be a string or object");
if (!oDomRef) {
throw new Error("oDomRef must not be null");
}
// oDomRef might be (and actually IS in most cases!) a string (the ID of a DOM element)
if (typeof (oDomRef) === "string") {
var id = oDomRef;
if (id == STATIC_UIAREA_ID) {
oDomRef = UIArea.getStaticAreaRef();
} else {
oDomRef = document.getElementById(oDomRef);
if (!oDomRef) {
throw new Error("DOM element with ID '" + id + "' not found in page, but application tries to insert content.");
}
}
}
// if the domref does not have an ID or empty ID => generate one
if (!oDomRef.id || oDomRef.id.length == 0) {
oDomRef.id = uid();
}
// create a new or fetch an existing UIArea
var sId = oDomRef.id;
var oUIArea = UIArea.registry.get(sId);
if (!oUIArea) {
oUIArea = new UIArea(oDomRef);
if (oCore && !isEmptyObject(oCore.oModels)) {
var oProperties = {
oModels: Object.assign({}, oCore.oModels),
oBindingContexts: {},
aPropagationListeners: []
};
oUIArea._propagateProperties(true, oUIArea, oProperties, true);
}
} else {
// this should solve the issue of 'recreation' of a UIArea
// e.g. via setRoot with a new domRef
oUIArea._setRootNode(oDomRef);
}
return oUIArea;
};
/**
* Returns the static, hidden area DOM element belonging to this core instance.
*
* It can be used e.g. for hiding elements like Popups, Shadow, Blocklayer etc.
*
* If it is not yet available, a DIV is created and appended to the body.
*
* @returns {Element} the static, hidden area DOM element belonging to this core instance.
* @throws {Error} an Error if the document is not yet ready
* @private
* @ui5-restricted sap.ui.core
*/
UIArea.getStaticAreaRef = function() {
if (!bDomReady) {
throw new Error("DOM is not ready yet. Static UIArea cannot be created.");
}
var oStaticArea = document.getElementById(STATIC_UIAREA_ID), oFirstFocusElement;
if (!oStaticArea) {
oStaticArea = document.createElement("div");
oFirstFocusElement = document.createElement("span");
oStaticArea.setAttribute("id", STATIC_UIAREA_ID);
Object.assign(oStaticArea.style, {
"height": "0",
"width": "0",
"overflow": "hidden",
"float": Configuration.getRTL() ? "right" : "left"
});
oFirstFocusElement.setAttribute("id", STATIC_UIAREA_ID + "-firstfe");
oFirstFocusElement.setAttribute("tabindex", -1);
oFirstFocusElement.style.fontSize = 0;
oStaticArea.appendChild(oFirstFocusElement);
document.body.insertBefore(oStaticArea, document.body.firstChild);
UIArea.create(oStaticArea).bInitial = false;
}
return oStaticArea;
};
/**
* Checks whether the given DOM element is the root of the static area.
*
* @param {Element} oDomRef DOM element to check
* @returns {boolean} Whether the given DOM element is the root of the static area
* @private
*/
UIArea.isStaticAreaRef = function(oDomRef) {
return !!(oDomRef && (oDomRef.id === STATIC_UIAREA_ID));
};
/**
* Sets the Core instance in Core onInit
* @param {sap.ui.core.Core} oCoreInstance the Core instance
* @private
*/
UIArea.setCore = function(oCoreInstance) {
oCore = oCoreInstance;
var aUiAreas = Configuration.getValue("areas");
// create any pre-configured UIAreas
if ( aUiAreas ) {
for (var i = 0, l = aUiAreas.length; i < l; i++) {
UIArea.create(aUiAreas[i]);
}
}
};
/**
* Registry of all <code>sap.ui.core.Element</code>s that currently exist.
*
* @namespace sap.ui.core.UIArea.registry
* @public
* @since 1.107
*/
/**
* Number of existing UIAreas.
*
* @type {int}
* @readonly
* @name sap.ui.core.UIArea.registry.size
* @public
*/
/**
* Return an object with all instances of <code>sap.ui.core.UIArea</code>,
* keyed by their ID.
*
* Each call creates a new snapshot object. Depending on the size of the UI,
* this operation therefore might be expensive. Consider to use the <code>forEach</code>
* or <code>filter</code> method instead of executing similar operations on the returned
* object.
*
* <b>Note</b>: The returned object is created by a call to <code>Object.create(null)</code>,
* and therefore lacks all methods of <code>Object.prototype</code>, e.g. <code>toString</code> etc.
*
* @returns {Object<sap.ui.core.ID,sap.ui.core.UIArea>} Object with all UIAreas, keyed by their ID
* @name sap.ui.core.UIArea.registry.all
* @function
* @public
*/
/**
* Retrieves an UIArea by its ID.
*
* When the ID is <code>null</code> or <code>undefined</code> or when there's no UIArea with
* the given ID, then <code>undefined</code> is returned.
*
* @param {sap.ui.core.ID} id ID of the UIArea to retrieve
* @returns {sap.ui.core.UIArea|undefined} UIArea with the given ID or <code>undefined</code>
* @name sap.ui.core.UIArea.registry.get
* @function
* @public
*/
/**
* Calls the given <code>callback</code> for each UIArea.
*
* The expected signature of the callback is
* <pre>
* function callback(oUIArea, sID)
* </pre>
* where <code>oUIArea</code> is the currently visited UIArea instance and <code>sID</code>
* is the ID of that instance.
*
* The order in which the callback is called for UIAreas is not specified and might change between
* calls (over time and across different versions of UI5).
*
* If UIAreas are created or destroyed within the <code>callback</code>, then the behavior is
* not specified. Newly added objects might or might not be visited. When an UIArea is destroyed or
* the root node is changed during the filtering and was not visited yet, it might or might not be
* visited. As the behavior for such concurrent modifications is not specified, it may change in
* newer releases.
*
* If a <code>thisArg</code> is given, it will be provided as <code>this</code> context when calling
* <code>callback</code>. The <code>this</code> value that the implementation of <code>callback</code>
* sees, depends on the usual resolution mechanism. E.g. when <code>callback</code> was bound to some
* context object, that object wins over the given <code>thisArg</code>.
*
* @param {function(sap.ui.core.UIArea,sap.ui.core.ID)} callback
* Function to call for each UIArea
* @param {Object} [thisArg=undefined]
* Context object to provide as <code>th