UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,288 lines (1,129 loc) 82.1 kB
/*! * 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(/&#xa;/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: