@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,288 lines (1,129 loc) • 82.1 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides the render manager sap.ui.core.RenderManager
sap.ui.define([
'./LabelEnablement',
'sap/ui/base/Object',
'sap/ui/performance/trace/Interaction',
'sap/base/util/uid',
"sap/ui/util/ActivityDetection",
"sap/ui/thirdparty/jquery",
"sap/base/security/encodeXML",
"sap/base/security/encodeCSS",
"sap/base/assert",
"sap/ui/performance/Measurement",
"sap/base/Log",
"sap/base/util/extend",
"./InvisibleRenderer",
"./Patcher"
], function(
LabelEnablement,
BaseObject,
Interaction,
uid,
ActivityDetection,
jQuery,
encodeXML,
encodeCSS,
assert,
Measurement,
Log,
extend,
InvisibleRenderer,
Patcher
) {
"use strict";
var aCommonMethods = ["renderControl", "cleanupControlWithoutRendering", "accessibilityState", "icon"];
var aStrInterfaceMethods = ["write", "writeEscaped", "writeAcceleratorKey", "writeControlData", "writeElementData",
"writeAttribute", "writeAttributeEscaped", "addClass", "writeClasses", "addStyle", "writeStyles",
"writeAccessibilityState", "writeIcon", "translate", "getConfiguration", "getHTML"];
var aDomInterfaceMethods = ["openStart", "voidStart", "attr", "class", "style", "openEnd", "voidEnd", "text", "unsafeHtml", "close"];
var aNonRendererMethods = ["render", "flush", "destroy"];
/**
* Creates an instance of the RenderManager.
*
* Applications or controls must not call the <code>RenderManager</code> constructor on their own
* but should use the {@link sap.ui.core.Core#createRenderManager sap.ui.getCore().createRenderManager()}
* method to create an instance for their exclusive use.
*
* @class A class that handles the rendering of controls.
*
* For the default rendering task of UI5, a shared RenderManager is created and owned by <code>sap.ui.core.Core</code>.
* Controls or other code that want to render controls outside the default rendering task
* can create a private instance of RenderManager by calling the
* {@link sap.ui.core.Core#createRenderManager sap.ui.getCore().createRenderManager()} method.
* When such a private instance is no longer needed, it should be {@link #destroy destroyed}.
*
* Control renderers only have access to a subset of the public and protected instance methods of
* this class. The instance methods {@link #flush}, {@link #render} and {@link #destroy} are not part
* of that subset and are reserved to the owner of the corresponding RenderManager instance.
* Renderers will use the provided methods to create their HTML output. The RenderManager will
* collect the HTML output and inject the final HTML DOM at the desired location.
*
*
* <h3>Renderers</h3>
* When the {@link #renderControl} method of the RenderManager is invoked, it will retrieve
* the default renderer for that control. By convention, the default renderer is implemented in its
* own namespace (static class) which matches the name of the control's class with the additional
* suffix 'Renderer'. So for a control <code>sap.m.Input</code> the default renderer will be searched
* for under the global name <code>sap.m.Input<i>Renderer</i></code>.
*
* <h3>In-place DOM patching</h3>
* As of 1.67, <code>RenderManager</code> provides a set of new APIs to describe the structure of the DOM that can be used by the control renderers.
*
* <pre>
*
* myButtonRenderer.render = function(rm, oButton) {
*
* rm.openStart("button", oButton);
* rm.attr("tabindex", 1);
* rm.class("myButton");
* rm.style("width", oButton.getWidth());
* rm.openEnd();
* rm.text(oButton.getText());
* rm.close("button");
*
* };
*
* </pre>
*
* By default, when the control is invalidated (e.g. a property is changed, an aggregation is removed, or an
* association is added), it will be registered for re-rendering. During the (re)rendering, the <code>render</code>
* method of the control renderer is executed via a specified <code>RenderManager</code> interface and the control
* instance.
*
* Traditional string-based rendering creates a new HTML structure of the control in every rendering cycle and removes
* the existing control DOM structure from the DOM tree.
*
* The set of new semantic <code>RenderManager</code> APIs lets us understand the structure of the DOM, walk along the
* live DOM tree, and figure out changes as new APIs are called. If there is a change, then <code>RenderManager</code>
* patches only the required parts of the live DOM tree. This allows control developers to remove their DOM-related
* custom setters.
*
* <b>Note:</b> To enable the new in-place rendering technology, the <code>apiVersion</code> property of the control
* renderer must be set to <code>2</code>. This property is not inherited by subclass renderers. It has to be set
* anew by each subclass to assure that the extended contract between framework and renderer is fulfilled (see next
* paragraph).
*
* <pre>
*
* var myButtonRenderer = {
* apiVersion: 2 // enable in-place DOM patching
* };
*
* myButtonRenderer.render = function(rm, oButton) {
*
* rm.openStart("button", oButton);
* ...
* ...
* rm.close("button");
*
* };
*
* </pre>
*
* <h3>Contract for Renderer.apiVersion 2</h3>
* To allow a more efficient in-place DOM patching and to ensure the compatibility of the control, the following
* prerequisites must be fulfilled for the controls using the new rendering technology:
*
* <ul>
* <li>Legacy control renderers must be migrated to the new semantic renderer API:
* {@link sap.ui.core.RenderManager#openStart openStart},
* {@link sap.ui.core.RenderManager#voidStart voidStart},
* {@link sap.ui.core.RenderManager#style style},
* {@link sap.ui.core.RenderManager#class class},
* {@link sap.ui.core.RenderManager#attr attr},
* {@link sap.ui.core.RenderManager#openEnd openEnd},
* {@link sap.ui.core.RenderManager#voidEnd voidEnd},
* {@link sap.ui.core.RenderManager#text text},
* {@link sap.ui.core.RenderManager#unsafeHtml unsafeHtml},
* {@link sap.ui.core.RenderManager#icon icon},
* {@link sap.ui.core.RenderManager#accessibilityState accessibilityState},
* {@link sap.ui.core.RenderManager#renderControl renderControl},
* {@link sap.ui.core.RenderManager#cleanupControlWithoutRendering cleanupControlWithoutRendering}
* </li>
* <li>During the migration, restrictions that are defined in the API documentation of those methods must be taken
* into account, e.g. tag and attribute names must be set in their canonical form.</li>
* <li>Fault tolerance of HTML5 markup is not applicable for the new semantic rendering API, e.g. except void tags,
* all tags must be closed; duplicate attributes within one HTML element must not exist.</li>
* <li>Existing control DOM structure will not be removed from the DOM tree; therefore all custom events, including
* the ones that are registered with jQuery, must be de-registered correctly at the <code>onBeforeRendering</code>
* and <code>exit</code> hooks.</li>
* <li>Classes and attribute names must not be escaped.</li>
* <li>Styles should be validated via types (e.g. <code>sap.ui.core.CSSSize</code>). But this might not be sufficient
* in all cases, e.g. validated URL values can contain harmful content; in this case
* {@link module:sap/base/security/encodeCSS encodeCSS} can be used.</li>
* <li>To allow a more efficient DOM update, second parameter of the {@link sap.ui.core.RenderManager#openStart openStart}
* or {@link sap.ui.core.RenderManager#voidStart voidStart} methods must be used to identify elements, e.g. use
* <code>rm.openStart("div", oControl.getId() + "-suffix");</code> instead of
* <code>rm.openStart("div").attr("id", oControl.getId() + "-suffix");</code></li>
* <li>Controls that listen to the <code>focusin</code> event must double check their focus handling. Since DOM nodes
* are not removed and only reused, the <code>focusin</code> event might not be fired during re-rendering.</li>
* </ul>
*
*
* @see sap.ui.core.Core
* @see sap.ui.getCore
*
* @borrows sap.ui.core.RenderManager#writeAccessibilityState as #accessibilityState
* @borrows sap.ui.core.RenderManager#writeIcon as #icon
*
* @extends Object
* @author SAP SE
* @version 1.87.1
* @alias sap.ui.core.RenderManager
* @public
*/
function RenderManager() {
var that = this,
oFocusHandler,
aBuffer,
aRenderedControls,
aStyleStack,
aRenderStack,
bLocked,
sOpenTag = "", // stores the last open tag that is used for the validation
bVoidOpen = false, // specifies whether the last open tag is a void tag or not
bDomInterface, // specifies the rendering interface that is used by the control renderers
sLegacyRendererControlId = "", // stores the id of the control that has a legacy renderer while its parent has the new semantic renderer
oStringInterface = {}, // holds old string based rendering API and the string implementation of the new semantic rendering API
oDomInterface = {}; // semantic rendering API for the controls whose renderer provides apiVersion=2 marker
/**
* Sets the focus handler to be used by the RenderManager.
*
* @param {sap.ui.core.FocusHandler} oNewFocusHandler the focus handler to be used.
* @private
*/
this._setFocusHandler = function(oNewFocusHandler) {
assert(oNewFocusHandler && BaseObject.isA(oNewFocusHandler, 'sap.ui.core.FocusHandler'), "oFocusHandler must be an sap.ui.core.FocusHandler");
oFocusHandler = oNewFocusHandler;
};
/**
* Reset all rendering related buffers.
*/
function reset() {
aBuffer = that.aBuffer = [];
aRenderedControls = that.aRenderedControls = [];
aStyleStack = that.aStyleStack = [{}];
bDomInterface = undefined;
bVoidOpen = false;
sOpenTag = "";
}
//#################################################################################################
// Assertion methods for validating Semantic Rendering API calls
// These methods will be converted to inline asserts when assertion removal is supported
//#################################################################################################
function assertValidName(sName, sField) {
assert(sName && typeof sName == "string" && /^[a-z_][a-zA-Z0-9_\-]*$/.test(sName), "The " + sField + " name provided '" + sName + "' is not valid; it must contain alphanumeric characters, hyphens or underscores");
}
function assertOpenTagHasStarted(sMethod) {
assert(sOpenTag, "There is no open tag; '" + sMethod + "' must not be called without an open tag");
}
function assertOpenTagHasEnded(bCustomAssertion) {
var bAssertion = (bCustomAssertion === undefined) ? !sOpenTag : bCustomAssertion;
assert(bAssertion, "There is an open tag; '" + sOpenTag + "' tag has not yet ended with '" + (bVoidOpen ? "voidEnd" : "openEnd") + "'");
}
function assertValidAttr(sAttr) {
assertValidName(sAttr, "attr");
assert(sAttr != "class" && sAttr != "style", "Attributes 'class' and 'style' must not be written, instead use dedicated 'class' or 'style' method");
}
function assertValidClass(sClass) {
assert(typeof sClass == "string" && !/\s/.test(sClass) && arguments.length === 1, "Method 'class' must be called with exactly one class name");
}
function assertValidStyle(sStyle) {
assert(sStyle && typeof sStyle == "string" && !/\s/.test(sStyle), "Method 'style' must be called with a non-empty string name");
}
//#################################################################################################
// Methods for 'Buffered writer' functionality... (all public)
// i.e. used methods in render-method of Renderers
//#################################################################################################
/**
* Write the given texts to the buffer
* @param {...string|number} sText (can be a number too)
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @SecSink {*|XSS}
*/
this.write = function(/** string|number */ sText /* ... */) {
assert(( typeof sText === "string") || ( typeof sText === "number"), "sText must be a string or number");
aBuffer.push.apply(aBuffer, arguments);
return this;
};
/**
* Escape text for HTML and write it to the buffer.
*
* For details about the escaping refer to {@link jQuery.sap.encodeHTML}
*
* @param {any} sText the text to escape
* @param {boolean} bLineBreaks Whether to convert line breaks into <br> tags
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
this.writeEscaped = function(sText, bLineBreaks) {
if ( sText != null ) {
sText = encodeXML( String(sText) );
if (bLineBreaks) {
sText = sText.replace(/
/g, "<br>");
}
aBuffer.push(sText);
}
return this;
};
/**
* Writes the attribute and its value into the HTML.
*
* For details about the escaping refer to {@link jQuery.sap.encodeHTML}
*
* @param {string} sName Name of the attribute
* @param {string | number | boolean} vValue Value of the attribute
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @SecSink {0 1|XSS} Attributes are written to HTML without validation
*/
this.writeAttribute = function(sName, vValue) {
assert(typeof sName === "string", "sName must be a string");
assert(typeof vValue === "string" || typeof vValue === "number" || typeof vValue === "boolean", "value must be a string, number or boolean");
aBuffer.push(" ", sName, "=\"", vValue, "\"");
return this;
};
/**
* Writes the attribute and a value into the HTML, the value will be encoded.
*
* The value is properly encoded to avoid XSS attacks.
*
* @param {string} sName Name of the attribute
* @param {any} vValue Value of the attribute
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @SecSink {0|XSS}
*/
this.writeAttributeEscaped = function(sName, vValue) {
assert(typeof sName === "string", "sName must be a string");
aBuffer.push(" ", sName, "=\"", encodeXML(String(vValue)), "\"");
return this;
};
/**
* Adds a style property to the style collection if the value is not empty or null
* The style collection is flushed if it is written to the buffer using {@link #writeStyle}
*
* @param {string} sName Name of the CSS property to write
* @param {string|float|int} vValue Value to write
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @SecSink {0 1|XSS} Styles are written to HTML without validation
*/
this.addStyle = function(sName, vValue) {
assert(typeof sName === "string", "sName must be a string");
if (vValue != null && vValue != "") {
assert((typeof vValue === "string" || typeof vValue === "number"), "value must be a string or number");
var oStyle = aStyleStack[aStyleStack.length - 1];
if (!oStyle.aStyle) {
oStyle.aStyle = [];
}
oStyle.aStyle.push(sName + ": " + vValue + ";");
}
return this;
};
/**
* Writes and flushes the style collection
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
this.writeStyles = function() {
var oStyle = aStyleStack[aStyleStack.length - 1];
if (oStyle.aStyle && oStyle.aStyle.length) {
this.writeAttribute("style", oStyle.aStyle.join(" "));
}
oStyle.aStyle = null;
return this;
};
/**
* Adds a class to the class collection if the name is not empty or null.
* The class collection is flushed if it is written to the buffer using {@link #writeClasses}
*
* @param {string} sName name of the class to be added; null values are ignored
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @SecSink {0|XSS} Classes are written to HTML without validation
*/
this.addClass = function(sName) {
if (sName) {
assert(typeof sName === "string", "sName must be a string");
var oStyle = aStyleStack[aStyleStack.length - 1];
if (!oStyle.aClasses) {
oStyle.aClasses = [];
}
oStyle.aClasses.push(sName);
}
return this;
};
/**
* Writes and flushes the class collection (all CSS classes added by "addClass()" since the last flush).
* Also writes the custom style classes added by the application with "addStyleClass(...)". Custom classes are
* added by default from the currently rendered control. If an oElement is given, this Element's custom style
* classes are added instead. If oElement === false, no custom style classes are added.
*
* @param {sap.ui.core.Element | boolean} [oElement] an Element from which to add custom style classes (instead of adding from the control itself)
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
this.writeClasses = function(oElement) {
assert(!oElement || typeof oElement === "boolean" || BaseObject.isA(oElement, 'sap.ui.core.Element'), "oElement must be empty, a boolean, or an sap.ui.core.Element");
var oStyle = aStyleStack[aStyleStack.length - 1];
// Custom classes are added by default from the currently rendered control. If an oElement is given, this Element's custom style
// classes are added instead. If oElement === false, no custom style classes are added.
var aCustomClasses;
if (oElement) {
aCustomClasses = oElement.aCustomStyleClasses;
} else if (oElement === false) {
aCustomClasses = [];
} else {
aCustomClasses = oStyle.aCustomStyleClasses;
}
if (oStyle.aClasses || aCustomClasses) {
var aClasses = [].concat(oStyle.aClasses || [], aCustomClasses || []);
if (aClasses.length) {
this.writeAttribute("class", aClasses.join(" "));
}
}
if (!oElement) {
oStyle.aCustomStyleClasses = null;
}
oStyle.aClasses = null;
return this;
};
//#################################################################################################
// Semantic Rendering Interface for String Based Rendering
//#################################################################################################
/**
* Opens the start tag of an HTML element.
*
* This must be followed by <code>openEnd</code> and concluded with <code>close</code>.
* To allow a more efficient DOM update, all tag names have to be used in their canonical form.
* For HTML elements, {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element tag names} must all be set in lowercase.
* For foreign elements, such as SVG, {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element tag names} can be set in upper camel case (e.g. linearGradient).
*
* @param {string} sTagName Tag name of the HTML element
* @param {sap.ui.core.Element|sap.ui.core.ID} [vControlOrId] Control instance or ID to identify the element
* @return {this} Reference to <code>this</code> in order to allow method chaining
*
* @public
* @since 1.67
*/
this.openStart = function(sTagName, vControlOrId) {
assertValidName(sTagName, "tag");
assertOpenTagHasEnded();
sOpenTag = sTagName;
this.write("<" + sTagName);
if (vControlOrId) {
if (typeof vControlOrId == "string") {
this.attr("id", vControlOrId);
} else {
this.writeElementData(vControlOrId);
}
}
return this;
};
/**
* Ends an open tag started with <code>openStart</code>.
*
* This indicates that there are no more attributes to set to the open tag.
*
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.openEnd = function(bExludeStyleClasses /* private */) {
assertOpenTagHasStarted("openEnd");
assertOpenTagHasEnded(!bVoidOpen);
assert(bExludeStyleClasses === undefined || bExludeStyleClasses === true, "The private parameter bExludeStyleClasses must be true or omitted!");
sOpenTag = "";
this.writeClasses(bExludeStyleClasses === true ? false : undefined);
this.writeStyles();
this.write(">");
return this;
};
/**
* Closes an open tag started with <code>openStart</code> and ended with <code>openEnd</code>.
*
* This indicates that there are no more children to append to the open tag.
*
* @param {string} sTagName Tag name of the HTML element
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.close = function(sTagName) {
assertValidName(sTagName, "tag");
assertOpenTagHasEnded();
this.write("</" + sTagName + ">");
return this;
};
/**
* Starts a self-closing tag, such as <code>img</code> or <code>input</code>.
*
* This must be followed by <code>voidEnd</code>. For self-closing tags, the <code>close</code> method must not be called.
* To allow a more efficient DOM update, void tag names have to be set in lowercase.
* This API is specific for void elements and must not be used for foreign elements.
* For more information, see {@link https://www.w3.org/TR/html5/syntax.html#void-elements}.
*
* @param {string} sTagName Tag name of the HTML element
* @param {sap.ui.core.Element|sap.ui.core.ID} [vControlOrId] Control instance or ID to identify the element
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.voidStart = function (sTagName, vControlOrId) {
this.openStart(sTagName, vControlOrId);
bVoidOpen = true;
return this;
};
/**
* Ends an open self-closing tag started with <code>voidStart</code>.
*
* This indicates that there are no more attributes to set to the open tag.
* For self-closing tags <code>close</code> must not be called.
*
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.voidEnd = function (bExludeStyleClasses /* private */) {
assertOpenTagHasStarted("voidEnd");
assertOpenTagHasEnded(bVoidOpen || !sOpenTag);
bVoidOpen = false;
sOpenTag = "";
this.writeClasses(bExludeStyleClasses ? false : undefined);
this.writeStyles();
this.write(">");
return this;
};
/**
* Sets the given HTML markup without any encoding or sanitizing.
*
* This must not be used for plain texts; use the <code>text</code> method instead.
*
* @param {string} sHtml Well-formed, valid HTML markup
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
* @SecSink {*|XSS}
*/
this.unsafeHtml = function(sHtml) {
assertOpenTagHasEnded();
this.write(sHtml);
return this;
};
/**
* Sets the text content with the given text.
*
* @param {string} sText The text to be written
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.text = function(sText) {
assertOpenTagHasEnded();
this.writeEscaped(sText);
return this;
};
/**
* Adds an attribute name-value pair to the last open HTML element.
*
* This is only valid when called between <code>openStart/voidStart</code> and <code>openEnd/voidEnd</code>.
* The attribute name must not be equal to <code>style</code> or <code>class</code>.
* Styles and classes must be set via dedicated <code>class</code> or <code>style</code> methods.
* To update the DOM correctly, all attribute names have to be used in their canonical form.
* For HTML elements, {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes attribute names} must all be set in lowercase.
* For foreign elements, such as SVG, {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute attribute names} can be set in upper camel case (e.g. viewBox).
*
* @param {string} sName Name of the attribute
* @param {*} vValue Value of the attribute
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.attr = function(sName, vValue) {
assertValidAttr(sName);
this.writeAttributeEscaped(sName, vValue);
return this;
};
/**
* Adds a class name to the class collection of the last open HTML element.
*
* This is only valid when called between <code>openStart/voidStart</code> and <code>openEnd/voidEnd</code>.
* Class name must not contain any whitespace.
*
* @param {string} sClass Class name to be written
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.class = function(sClass) {
if (sClass) {
assertValidClass.apply(this, arguments);
this.addClass(encodeXML(sClass));
}
return this;
};
/**
* Adds a style name-value pair to the style collection of the last open HTML element.
*
* This is only valid when called between <code>openStart/voidStart</code> and <code>openEnd/voidEnd</code>.
*
* @param {string} sName Name of the style property
* @param {string} sValue Value of the style property
* @return {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.67
*/
this.style = function(sName, sValue) {
assertValidStyle(sName);
this.addStyle(sName, sValue);
return this;
};
// @borrows sap.ui.core.RenderManager#writeAccessibilityState as accessibilityState
this.accessibilityState = this.writeAccessibilityState;
// @borrows sap.ui.core.RenderManager#writeIcon as icon
this.icon = this.writeIcon;
//#################################################################################################
// Semantic Rendering Interface for DOM Based Rendering
//#################################################################################################
// @see sap.ui.core.RenderManager#openStart
oDomInterface.openStart = function(sTagName, vControlOrId) {
assertValidName(sTagName, "tag");
assertOpenTagHasEnded();
sOpenTag = sTagName;
if (!vControlOrId) {
Patcher.openStart(sTagName);
} else if (typeof vControlOrId == "string") {
Patcher.openStart(sTagName, vControlOrId);
} else {
Patcher.openStart(sTagName, vControlOrId.getId());
renderElementData(this, vControlOrId);
}
return this;
};
// @see sap.ui.core.RenderManager#voidStart
oDomInterface.voidStart = function(sTagName, vControlOrId) {
this.openStart(sTagName, vControlOrId);
bVoidOpen = true;
return this;
};
// @see sap.ui.core.RenderManager#attr
oDomInterface.attr = function(sName, vValue) {
assertValidAttr(sName);
assertOpenTagHasStarted("attr");
Patcher.attr(sName, vValue);
return this;
};
// @see sap.ui.core.RenderManager#class
oDomInterface.class = function(sClass) {
if (sClass) {
assertValidClass.apply(this, arguments);
assertOpenTagHasStarted("class");
Patcher.class(sClass);
}
return this;
};
// @see sap.ui.core.RenderManager#style
oDomInterface.style = function(sName, vValue) {
assertValidStyle(sName);
assertOpenTagHasStarted("style");
Patcher.style(sName, vValue);
return this;
};
// @see sap.ui.core.RenderManager#openEnd
oDomInterface.openEnd = function(bExludeStyleClasses /* private */) {
if (bExludeStyleClasses !== true) {
var oStyle = aStyleStack[aStyleStack.length - 1];
var aStyleClasses = oStyle.aCustomStyleClasses;
if (aStyleClasses) {
aStyleClasses.forEach(Patcher.class, Patcher);
oStyle.aCustomStyleClasses = null;
}
}
assertOpenTagHasStarted("openEnd");
assertOpenTagHasEnded(!bVoidOpen);
assert(bExludeStyleClasses === undefined || bExludeStyleClasses === true, "The private parameter bExludeStyleClasses must be true or omitted!");
sOpenTag = "";
Patcher.openEnd();
return this;
};
// @see sap.ui.core.RenderManager#voidEnd
oDomInterface.voidEnd = function(bExludeStyleClasses /* private */) {
if (!bExludeStyleClasses) {
var oStyle = aStyleStack[aStyleStack.length - 1];
var aStyleClasses = oStyle.aCustomStyleClasses;
if (aStyleClasses) {
aStyleClasses.forEach(Patcher.class, Patcher);
oStyle.aCustomStyleClasses = null;
}
}
assertOpenTagHasStarted("voidEnd");
assertOpenTagHasEnded(bVoidOpen || !sOpenTag);
bVoidOpen = false;
sOpenTag = "";
Patcher.voidEnd();
return this;
};
// @see sap.ui.core.RenderManager#text
oDomInterface.text = function(sText) {
assertOpenTagHasEnded();
if (sText != null) {
Patcher.text(sText);
}
return this;
};
// @see sap.ui.core.RenderManager#unsafeHtml
oDomInterface.unsafeHtml = function(sHtml) {
assertOpenTagHasEnded();
Patcher.unsafeHtml(sHtml);
return this;
};
// @see sap.ui.core.RenderManager#close
oDomInterface.close = function(sTagName) {
assertValidName(sTagName, "tag");
assertOpenTagHasEnded();
Patcher.close(sTagName);
return this;
};
//Triggers the BeforeRendering event on the given Control
function triggerBeforeRendering(oControl){
bLocked = true;
try {
var oEvent = new jQuery.Event("BeforeRendering");
// store the element on the event (aligned with jQuery syntax)
oEvent.srcControl = oControl;
oControl._handleEvent(oEvent);
} finally {
bLocked = false;
}
}
/**
* Cleans up the rendering state of the given control without rendering it.
*
* A control is responsible for the rendering of all its child controls.
* But in some cases it makes sense that a control only renders a subset of its children
* based on some criterion. For example, a typical carousel control might, for performance
* reasons, only render the currently visible children (and maybe some child before and
* after the visible area to facilitate slide-in / slide-out animations), but not all children.
* This leads to situations where a child had been rendered before, but shouldn't be rendered
* anymore after an update of the carousel's position. The DOM related state of that child then
* must be cleaned up correctly, e.g. by de-registering resize handlers or native event handlers.
* <code>cleanupControlWithoutRendering</code> helps with that task by triggering the same
* activities that the normal rendering triggers before the rendering of a control
* (e.g. it fires the <code>BeforeRendering</code> event). It just doesn't call the renderer
* and the control will not receive an <code>AfterRendering</code> event.
*
* The following example shows how <code>renderControl</code> and <code>cleanupControlWithoutRendering</code>
* should be used:
*
* <pre>
* CarouselRenderer.render = function(rm, oCarousel){
*
* ...
*
* oCarousel.getPages().forEach( function( oPage ) {
* if ( oCarousel.isPageToBeRendered( oPage ) ) {
* rm.renderControl( oPage ); // onBeforeRendering, render, later onAfterRendering
* } else {
* rm.cleanupControlWithoutRendering( oPage ); // onBeforeRendering
* }
* });
*
* ...
*
* };
* </pre>
*
* <h3>DOM Removal</h3>
* The method does not remove the DOM of the given control. The caller of this method has
* to take care to remove it at some later point in time. It should indeed be <i>later</i>,
* not <i>before</i> as the <code>onBeforeRendering</code> hook of the control might need
* access to the old DOM for a proper cleanup.
*
* For parents which are rendered with the normal mechanism as shown in the example above,
* the removal of the old child DOM is guaranteed. The whole DOM of the parent control
* (including the DOM of the no longer rendered child) will be replaced with new DOM (no
* longer containing the child) when the rendering cycle finishes.
*
* <b>Note:</b>: the functionality of this method is different from the default handling for
* invisible controls (controls with <code>visible == false</code>). The standard rendering
* for invisible controls still renders a placeholder DOM. This allows re-rendering of the
* invisible control once it becomes visible again without a need to render its parent, too.
* Children that are cleaned up with this method here, are supposed to have no more DOM at all.
* Rendering them later on therefore requires an involvement (typically: a rendering) of
* their parent.
*
* @param {sap.ui.core.Control} oControl Control that should be cleaned up
* @public
* @since 1.22.9
*/
this.cleanupControlWithoutRendering = function(oControl) {
assert(!oControl || BaseObject.isA(oControl, 'sap.ui.core.Control'), "oControl must be an sap.ui.core.Control or empty");
if (!oControl) {
return;
}
var oDomRef = oControl.getDomRef();
if (oDomRef) {
// Call beforeRendering to allow cleanup
triggerBeforeRendering(oControl);
// as children are not visited during rendering, their DOM has to be preserved here
RenderManager.preserveContent(oDomRef, /* bPreserveRoot */ false, /* bPreserveNodesWithId */ false);
// Preserved controls still need to be alive
if (!oDomRef.hasAttribute(ATTR_PRESERVE_MARKER)) {
oControl.bOutput = false;
}
}
};
/**
* Turns the given control into its HTML representation and appends it to the
* rendering buffer.
*
* If the given control is undefined or null, then nothing is rendered.
*
* @param {sap.ui.core.Control} oControl the control that should be rendered
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
this.renderControl = function(oControl) {
assert(!oControl || BaseObject.isA(oControl, 'sap.ui.core.Control'), "oControl must be an sap.ui.core.Control or empty");
// don't render a NOTHING
if (!oControl) {
return this;
}
// create stack to determine rendered parent
if (!aRenderStack) {
aRenderStack = [];
}
// stop the measurement of parent
if (aRenderStack && aRenderStack.length > 0) {
Measurement.pause(aRenderStack[0] + "---renderControl");
} else if (oControl.getParent() && oControl.getParent().getMetadata().getName() == "sap.ui.core.UIArea") {
Measurement.pause(oControl.getParent().getId() + "---rerender");
}
aRenderStack.unshift(oControl.getId());
// start performance measurement
Measurement.start(oControl.getId() + "---renderControl", "Rendering of " + oControl.getMetadata().getName(), ["rendering","control"]);
// Trigger onBeforeRendering before checking visibility, as visible property might be changed in handler
triggerBeforeRendering(oControl);
Measurement.pause(oControl.getId() + "---renderControl");
// don't measure getRenderer because if Load needed its measured in Ajax call
// Either render the control normally, or invoke the InvisibleRenderer in case the control
// uses the default visible property
var oRenderer;
var oMetadata = oControl.getMetadata();
var bVisible = oControl.getVisible();
if (bVisible) {
// If the control is visible, return its renderer (Should be the default case, just like before)
oRenderer = oMetadata.getRenderer();
} else {
// If the control is invisible, find out whether it uses its own visible implementation
var oVisibleProperty = oMetadata.getProperty("visible");
var bUsesDefaultVisibleProperty =
oVisibleProperty
&& oVisibleProperty._oParent
&& oVisibleProperty._oParent.getName() == "sap.ui.core.Control";
oRenderer = bUsesDefaultVisibleProperty
// If the control inherited its visible property from sap.ui.core.Control, use
// the default InvisibleRenderer to render a placeholder instead of the real
// control HTML
? InvisibleRenderer
// If the control has their own visible property or one not inherited from
// sap.ui.core.Control, return the real renderer
: oMetadata.getRenderer();
}
Measurement.resume(oControl.getId() + "---renderControl");
// unbind any generically bound browser event handlers
var aBindings = oControl.aBindParameters,
oDomRef;
// if we have stored bind calls and we have a DomRef
if (aBindings && aBindings.length > 0 && (oDomRef = oControl.getDomRef())) {
var $DomRef = jQuery(oDomRef);
for (var i = 0; i < aBindings.length; i++) {
var oParams = aBindings[i];
$DomRef.off(oParams.sEventType, oParams.fnProxy);
}
}
// Render the control using the RenderManager interface
if (oRenderer && typeof oRenderer.render === "function") {
// Determine the rendering interface
if (aBuffer.length) {
// string rendering has already started therefore we cannot use DOM rendering interface
bDomInterface = false;
} else if (bDomInterface === undefined) {
// rendering interface must be determined for the root control once per rendering
// depending on the DOM reference of the control within the DOM tree
oDomRef = oControl.getDomRef() || InvisibleRenderer.getDomRef(oControl);
// DOM based rendering is valid only for the controls that are already rendered and providing apiVersion=2 marker.
// If the control is in the preserved area then we should not use DOM rendering interface to avoid patching of preserved nodes.
if (oDomRef && RenderManager.getApiVersion(oRenderer) == 2 && !RenderManager.isPreservedContent(oDomRef)) {
// patching will happen during the control renderer calls therefore we need to get the focus info before the patching
oFocusHandler && oFocusHandler.storePatchingControlFocusInfo(oDomRef);
// set the starting point of the Patcher
Patcher.setRootNode(oDomRef);
// remember that we are using DOM based rendering interface
bDomInterface = true;
} else {
// DOM rendering is not possible we fall back to string rendering interface
bDomInterface = false;
}
} else if (!sLegacyRendererControlId && bDomInterface) {
// for every subsequent renderControl call we need to check whether we can continue with the DOM based rendering
if (RenderManager.getApiVersion(oRenderer) != 2) {
// remember the control id that we have to provide string rendering interface
sLegacyRendererControlId = oControl.getId();
bDomInterface = false;
}
}
// before the rendering get custom style classes of the control
var oControlStyles = {};
if (oControl.aCustomStyleClasses && oControl.aCustomStyleClasses.length > 0) {
oControlStyles.aCustomStyleClasses = oControl.aCustomStyleClasses;
}
// push them to the style stack that will be read by the first writeClasses/openEnd/voidEnd call to append additional classes
aStyleStack.push(oControlStyles);
// execute the renderer of the control via valid rendering interface
if (bDomInterface) {
// remember the cursor of the Patcher before the control renderer is executed
var oCurrentNode = Patcher.getCurrentNode();
// execute the control renderer with DOM rendering interface
oRenderer.render(oDomInterface, oControl);
// during the rendering the cursor of the Patcher should move to the next element when openStart or voidStart is called
// compare after rendering cursor with before rendering cursor to determine whether the control produced any output
if (Patcher.getCurrentNode() == oCurrentNode) {
// we need to remove the control DOM if there is no output produced
Patcher.unsafeHtml("", oControl.getId());
oControl.bOutput = false;
} else {
oControl.bOutput = true;
}
} else {
// remember the buffer size before the control renderer is executed
var iBufferLength = aBuffer.length;
// execute the control renderer with string rendering interface
oRenderer.render(oStringInterface, oControl);
// compare after rendering buffer size with before rendering buffer size to determine whether the control produced any output
oControl.bOutput = (aBuffer.length !== iBufferLength);
}
// pop from the style stack after rendering for the next control
aStyleStack.pop();
// at the end of the rendering apply the rendering buffer of the control that is forced to render string interface
if (sLegacyRendererControlId && sLegacyRendererControlId === oControl.getId()) {
Patcher.unsafeHtml(aBuffer.join(""), sLegacyRendererControlId);
sLegacyRendererControlId = "";
bDomInterface = true;
aBuffer = [];
}
} else {
Log.error("The renderer for class " + oMetadata.getName() + " is not defined or does not define a render function! Rendering of " + oControl.getId() + " will be skipped!");
}
// Remember the rendered control
aRenderedControls.push(oControl);
// let the UIArea know that this control has been rendered
// FIXME: RenderManager (RM) should not need to know about UIArea. Maybe UIArea should delegate rendering to RM
var oUIArea = oControl.getUIArea();
if (oUIArea) {
oUIArea._onControlRendered(oControl);
}
// Special case: If an invisible placeholder was rendered, use a non-boolean value
if (oRenderer === InvisibleRenderer) {
oControl.bOutput = "invisible"; // Still evaluates to true, but can be checked for the special case
}
// end performance measurement
Measurement.end(oControl.getId() + "---renderControl");
aRenderStack.shift();
// resume the measurement of parent
if (aRenderStack && aRenderStack.length > 0) {
Measurement.resume(aRenderStack[0] + "---renderControl");
} else if (oControl.getParent() && oControl.getParent().getMetadata().getName() == "sap.ui.core.UIArea") {
Measurement.resume(oControl.getParent().getId() + "---rerender");
}
return this;
};
/**
* Renders the given {@link sap.ui.core.Control} and finally returns
* the content of the rendering buffer.
* Ensures the buffer is restored to the state before calling this method.
*
* @param {sap.ui.core.Control}
* oControl the Control whose HTML should be returned.
* @return {string} the resulting HTML of the provided control
* @deprecated Since version 0.15.0. Use <code>flush()</code> instead render content outside the rendering phase.
* @public
*/
this.getHTML = function(oControl) {
assert(oControl && BaseObject.isA(oControl, 'sap.ui.core.Control'), "oControl must be an sap.ui.core.Control");
var tmp = aBuffer;
var aResult = aBuffer = this.aBuffer = [];
this.renderControl(oControl);
aBuffer = this.aBuffer = tmp;
return aResult.join("");
};
//Does everything needed after the rendering (restore focus, calling "onAfterRendering", initialize event binding)
function finalizeRendering(oStoredFocusInfo){
var i, size = aRenderedControls.length;
for (i = 0; i < size; i++) {
aRenderedControls[i]._sapui_bInAfterRenderingPhase = true;
}
bLocked = true;
try {
// Notify the behavior object that the controls will be attached to DOM
for (i = 0; i < size; i++) {
var oControl = aRenderedControls[i];
if (oControl.bOutput && oControl.bOutput !== "invisible") {
var oEvent = new jQuery.Event("AfterRendering");
// store the element on the event (aligned with jQuery syntax)
oEvent.srcControl = oControl;
// start performance measurement
Measurement.start(oControl.getId() + "---AfterRendering","AfterRendering of " + oControl.getMetadata().getName(), ["rendering","after"]);
oControl._handleEvent(oEvent);
// end performance measurement
Measurement.end(oControl.getId() + "---AfterRendering");
}
}
} finally {
for (i = 0; i < size; i++) {
delete aRenderedControls[i]._sapui_bInAfterRenderingPhase;
}
bLocked = false;
}
//finally restore focus
try {
oFocusHandler.restoreFocus(oStoredFocusInfo);
} catch (e) {
Log.warning("Problems while restoring the focus after rendering: " + e, null);
}
// Re-bind any generically bound browser event handlers (must happen after restoring focus to avoid focus event)
for (i = 0; i < size; i++) {
var oControl = aRenderedControls[i],
aBindings = oControl.aBindParameters,
oDomRef;
// if we have stored bind calls and we have a DomRef
if (aBindings && aBindings.length > 0 && (oDomRef = oControl.getDomRef())) {
var $DomRef = jQuery(oDomRef);
for (var j = 0; j < aBindings.length; j++) {
var oParams = aBindings[j];
$DomRef.on(oParams.sEventType, oParams.fnProxy);
}
}
}
}
function flushInternal(fnPutIntoDom, fnDone) {
var oStoredFocusInfo;
if (!bDomInterface) {
oStoredFocusInfo = oFocusHandler && oFocusHandler.getControlFocusInfo();
fnPutIntoDom(aBuffer.join(""));
} else {
oStoredFocusInfo = oFocusHandler && oFocusHandler.getPatchingControlFocusInfo();
Patcher.reset();
}
finalizeRendering(oStoredFocusInfo);
reset();
ActivityDetection.refresh();
if (fnDone) {
fnDone();
}
}
/**
* Renders the content of the rendering buffer into the provided DOM node.
*
* This function must not be called within control renderers.
*
* Usage:
* <pre>
*
* // Create a new instance of the RenderManager
* var rm = sap.ui.getCore().createRenderManager();
*
* // Use the writer API to fill the buffers
* rm.write(...);
* rm.renderControl(oControl);
* rm.write(...);
* ...
*
* // Finally flush the buffer into the provided DOM node (The current content is removed)
* rm.flush(oDomNode);
*
* // If the instance is not needed anymore, destroy it
* rm.destroy();
*
* </pre>
*
* @param {Element} oTargetDomNode Node in the DOM where the buffer should be flushed into
* @param {boolean} bDoNotPreserve Determines whether the content is preserved (<code>false</code>) or not (<code>true</code>)
* @param {boolean|int} vInsert Determines whether the buffer of the target DOM node is expanded (<code>true</code>) or
* replaced (<code>false</code>), or the new entry is inserted at a specific position
* (value of type <code>int</code>)
* @public
*/
this.flush = function(oTargetDomNode, bDoNotPreserve, vInsert) {
assert((typeof oTargetDomNode === "object") && (oTargetDomNode.ownerDocument == document), "oTargetDomNode must be a DOM element");
var fnDone = Interaction.notifyAsyncStep();
// preserve HTML content before flushing HTML into target DOM node
if (!bDoNotPreserve && (typeof vInsert !== "number") && !vInsert) { // expression mimics the conditions used below
RenderManager.preserveContent(oTargetDomNode);
}
flushInternal(function(sHTML) {
for (var i = 0; i < aRenderedControls.length; i++) {
//TODO It would be enough to loop over the controls for which renderControl was initially called but for this
//we have to manage an additional array. Rethink about later.
var oldDomNode = aRenderedControls[i].getDomRef();
if (oldDomNode && !RenderManager.isPreservedContent(oldDomNode)) {
if (RenderManager.isInlineTemplate(oldDomNode)) {
jQuery(oldDomNode).empty();
} else {
jQuery(oldDomNode).remove();
}
}
}
if (typeof vInsert === "number") {
if (vInsert <= 0) { // new HTML should be inserted at the beginning
jQuery(oTargetDomNode).prepend(sHTML);
} else { // new element should be inserted at a certain position > 0
var $predecessor = jQuery(oTargetDomNode).children().eq(vInsert - 1); // find the element which should be directly before the new one
if ($predecessor.length === 1) {
// element found - put the HTML in after this element
$predecessor.after(sHTML);
} else {
// element not found (this should not happen when properly used), append the new HTML
jQuery(oTargetDomNode).append(sHTML);
}
}
} else if (!vInsert) {
jQuery(oTargetDomNode).html(sHTML); // Put the HTML into the given DOM Node
} else {
jQuery(oTargetDomNode).append(sHTML); // Append the HTML into the given DOM Node
}
}, fnDone);
};
/**
* Renders the given control to the provided DOMNode.
*
* If the control is already rendered in the provided DOMNode the DOM of the control is replaced. If the control
* is already rendered somewhere else the current DOM of the control is removed and the new DOM is appended
* to the provided DOMNode.
*
* This function must not be called within control renderers.
*
* @param {sap.ui.core.Control} oControl the Control that should be rendered.
* @param {Element} oTargetDomNode The node in the DOM where the result of the rendering should be inserted.
* @public
*/
this.render = function(oControl, oTargetDomNode) {
assert(oControl && BaseObject.isA(oControl, 'sap.ui.core.Control'), "oControl must be a control");
assert(typeof oTargetDomNode === "object" && oTargetDomNode.ownerDocument == document, "oTargetDomNode must be a DOM element");
if ( bLocked ) {
Log.error("Render must not be called within Before or After Rendering Phase. Call ignored.", null, this);
return;
}
var fnDone = Interaction.notifyAsyncStep();
// Reset internal state before rendering
reset();
// Retrieve the markup (the rendering phase)
this.renderControl(oControl);
// FIXME: