UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

587 lines (517 loc) 20.7 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides in-place rendering module for the RenderManager sap.ui.define([ "sap/ui/Device" ], function(Device) { "use strict"; // points a dummy CSSStyleDeclaration for style validation purposes var oCSSStyleDeclaration = document.createElement("title").style; // stores a <template> element to convert HTML strings to a DocumentFragment var oTemplateElement = document.createElement("template"); /** * Provides custom mutators for attributes. * Custom mutators ensure that the attribute value is aligned with the property value. * * Mutator functions are executed before the properties are set or removed. * If the return value of the function is <code>true</code>, then the attribute will not be set. * * Default mutators are used to update DOM properties apart from attributes. * According to the IDL definition some HTML attributes have no 1:1 mapping to properties. * For more information, see {@link https://www.w3.org/TR/REC-DOM-Level-1/idl-definitions.html}. */ var AttributeMutators = { value: function(oElement, sNewValue) { if (oElement.tagName == "INPUT") { oElement.value = (sNewValue == null) ? "" : sNewValue; } }, checked: function(oElement, sNewValue) { if (oElement.tagName == "INPUT") { oElement.checked = (sNewValue == null) ? false : true; } }, selected: function(oElement, sNewValue) { if (oElement.tagName == "OPTION") { oElement.selected = (sNewValue == null) ? false : true; } } }; if (Device.browser.safari) { /* * Safari 14ff reports calls to Element.prototype.removeAttribute("style") as CSP violations, * if 'inline-style's are not allowed, see https://bugs.webkit.org/show_bug.cgi?id=227349#c3 * * Assigning the empty string as style cleans up the CSS, but not the DOM, therefore we apply * this fallback to Safari only. */ AttributeMutators.style = function(oElement, sNewValue) { if ( sNewValue == null ) { oElement.style = ""; return true; // skip removeAttribute } }; } /** * Creates an HTML element from the given tag name and parent namespace */ var createElement = function (sTagName, oParent) { if (sTagName == "svg") { return document.createElementNS("http://www.w3.org/2000/svg", "svg"); } var sNamespaceURI = oParent && oParent.namespaceURI; if (!sNamespaceURI || sNamespaceURI == "http://www.w3.org/1999/xhtml" || oParent.localName == "foreignObject") { return document.createElement(sTagName); } return document.createElementNS(sNamespaceURI, sTagName); }; /** * @class Creates a <code>Patcher</code> instance which can be used for in-place DOM patching. * * @alias sap.ui.core.Patcher * @class * @private * @ui5-restricted sap.ui.core.RenderManager */ var Patcher = function() { this._oRoot = null; // Root node where the patching is started this._oCurrent = null; // Current node being patched, this value is always up-to-date this._oParent = null; // Parent node of the current node being patched, this valule is not alway up-to-date this._oReference = null; // Reference node that corresponds to the position of the current node this._oNewElement = null; // Newly created element which is not yet inserted into the DOM tree this._oNewParent = null; // HTML element where the newly created element to be inserted this._oNewReference = null; // Reference element that corresponds to the position of the newly created element this._iTagOpenState = 0; // 0: Tag is Closed, 1: Tag is Open and just Created, has no attributes, 2: Tag is Open and Existing, might have attributes this._sStyles = ""; // Style collection of the current node this._sClasses = ""; // Class name collection of the current node this._mAttributes = Object.create(null); // Set of all attributes name-value pair of the current node }; /** * Sets the root node from which the patching will be started. * * The root node must be set once before calling any other APIs. * If the root node parameter is not provided, a <code>DocumentFragment</code> is created as the root node. * * @param {HTMLElement} [oRootNode] The DOM node from which the patching will be started */ Patcher.prototype.setRootNode = function(oRootNode) { if (this._oRoot) { this.reset(); } this._oRoot = oRootNode || document.createDocumentFragment(); }; /** * Returns the root node from which the patching was started or a <code>DocumentFragment</code> created as a root node. * * @return {Node} The root node of the Patcher */ Patcher.prototype.getRootNode = function() { return this._oRoot; }; /** * Returns the current node being patched. * * @returns {Node} The node being patched */ Patcher.prototype.getCurrentNode = function() { return this._oCurrent; }; /** * Cleans up the current patching references and makes the patcher ready for the next patching. */ Patcher.prototype.reset = function() { this._oRoot = this._oCurrent = this._oParent = this._oReference = this._oNewElement = this._oNewParent = this._oNewReference = null; this._iTagOpenState = 0; /* Tag is Closed */ }; /** * Sets the next node that is going to be patched. */ Patcher.prototype._walkOnTree = function() { this._oReference = null; if (!this._oCurrent) { // if the current node does not exist yet, that means we are on the first call after the root node is set if (this._oRoot.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { // for the initial rendering the Patcher creates a DocumentFragment to assemble all created DOM nodes within it // if there is nothing to patch the Patcher will start to create elements, here we do not set the current node to force the rendering starts // the first created element must be appended to the DocumentFragment, so let the parent be the DocumentFragment node this._oParent = this._oRoot; } else { // during the re-rendering, the root node points to where the patching must be started this._oParent = this._oRoot.parentNode; this._oCurrent = this._oRoot; } } else if (this._iTagOpenState /* Tag is Open */) { // a new tag is opened while the previous tag was already open e.g. <div><span this._oParent = this._oCurrent; this._oCurrent = this._oCurrent.firstChild; } else { // after the previous tag has been closed, a new tag is opened e.g. <div></div><span this._oParent = this._oCurrent.parentNode; this._oCurrent = this._oCurrent.nextSibling; } }; /** * Finds the matching HTML element from the given ID and moves the corresponding element to the correct location. */ Patcher.prototype._matchElement = function(sId) { if (!sId) { return; } // TODO: the element with the given ID might exists in the DOM tree // See the Patcher.qunit.js - Rendering:existing elements test if (!this._oCurrent) { return; } if (this._oCurrent.id == sId || this._oCurrent == this._oRoot) { return; } var oCurrent = document.getElementById(sId); if (oCurrent) { this._oCurrent = this._oParent.insertBefore(oCurrent, this._oCurrent); return; } if (this._oCurrent.id) { this._oReference = this._oCurrent; this._oCurrent = null; } }; /** * Checks whether the current node being patched matches the specified node name. * If there is no match, the old DOM node must be removed, and new nodes must be created. */ Patcher.prototype._matchNodeName = function(sNodeName) { if (!this._oCurrent) { return; } var sCurrentNodeName = (this._oCurrent.nodeType == 1 /* Node.ELEMENT_NODE */) ? this._oCurrent.localName : this._oCurrent.nodeName; if (sCurrentNodeName == sNodeName) { return; } this._oReference = this._oCurrent; this._oCurrent = null; }; /** * Gets and stores attributes of the current node. * * Using getAttributeNames along with getAttribute is a memory-efficient and performant alternative to accessing Element.attributes. * For more information, see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNames}. */ Patcher.prototype._getAttributes = function() { for (var i = 0, aAttributeNames = this._oCurrent.getAttributeNames(); i < aAttributeNames.length; i++) { this._mAttributes[aAttributeNames[i]] = this._oCurrent.getAttribute(aAttributeNames[i]); } }; /** * Stores the specified element that is going to be inserted into the document after patching has been completed. */ Patcher.prototype._setNewElement = function(oNewElement) { if (!oNewElement) { return; } if (!this._oNewElement) { this._oNewElement = this._oCurrent; this._oNewParent = this._oParent; this._oNewReference = this._oReference; } else { this._oParent.insertBefore(this._oCurrent, this._oReference); } }; /** * Inserts the stored new element into the document after patching has been completed. */ Patcher.prototype._insertNewElement = function() { if (this._oCurrent == this._oNewElement) { this._oNewParent[this._oNewReference == this._oRoot ? "replaceChild" : "insertBefore"](this._oNewElement, this._oNewReference); this._oNewElement = this._oNewParent = this._oNewReference = null; } }; /** * Indicates whether the <code>Patcher</code> is in creation or patching mode. * * @returns {boolean} */ Patcher.prototype.isCreating = function() { return Boolean(this._oNewElement); }; /** * Aligns the DOM node that is currently patched with the given DOM node that does not need patching. * * This method can be used to skip elements that do not need to be visited for patching. * If the callback is provided, then the Patcher informs the callback about the skipped node. The returned value of the callback * can be used to move the cursor of the Patcher on the DOM tree. This can be useful to skip multiple root nodes. * * @param {HTMLElement} oDomNode HTML element that needs to be aligned with the currently being patched node * @param {function} [fnCallback] The callback to be informed about the skipped node * @return {sap.ui.core.Patcher} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.alignWithDom = function(oDomNode, fnCallback) { this._walkOnTree(); if (!this._oCurrent || this._oCurrent.id != oDomNode.id || this._oParent != oDomNode.parentNode) { this._oCurrent = this._oParent.insertBefore(oDomNode, this._oCurrent); } if (fnCallback) { this._oCurrent = fnCallback(oDomNode) || this._oCurrent; } this._iTagOpenState = 0; /* Closed */ return this; }; /** * Opens the start tag of an HTML element. * * This must be followed by <code>openEnd</code> and concluded with <code>close</code>. * * @param {string} sTagName Tag name of the HTML element; all lowercase * @param {sap.ui.core.ID} [sId] ID to identify the element * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.openStart = function(sTagName, sId) { this._walkOnTree(); this._matchElement(sId); this._matchNodeName(sTagName); if (this._oCurrent) { this._getAttributes(); this._iTagOpenState = 2; /* Tag is Open and Existing */ } else { this._oCurrent = createElement(sTagName, this._oParent); this._setNewElement(this._oCurrent); this._iTagOpenState = 1; /* Tag is Open and Created */ } if (sId) { this.attr("id", sId); } return this; }; /** * Starts a self-closing tag, such as <code>img</code> or <code>input</code>. * * This must be followed by <code>voidEnd</code>. * * @param {string} sTagName Tag name of the HTML element; all lowercase * @param {sap.ui.core.ID} [sId] ID to identify the element * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.voidStart = Patcher.prototype.openStart; /** * Sets an attribute name-value pair to the current element. * * This is only valid when called between <code>openStart/voidStart</code> and <code>openEnd/voidEnd</code>. * Case-insensitive attribute names must all be set in lowercase. * * @param {string} sAttr Name of the attribute * @param {*} vValue Value of the attribute; any non-string value specified is converted automatically into a string * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.attr = function(sAttr, vValue) { if (sAttr === "style") { this._sStyles = vValue; return this; } if (this._iTagOpenState == 1 /* Tag is Open and Created */) { this._oCurrent.setAttribute(sAttr, vValue); return this; } var sNewValue = String(vValue); var sOldValue = this._mAttributes[sAttr]; var fnMutator = AttributeMutators[sAttr]; if (sOldValue !== undefined) { delete this._mAttributes[sAttr]; } if (fnMutator && fnMutator(this._oCurrent, sNewValue, sOldValue)) { return this; } if (sOldValue !== sNewValue) { this._oCurrent.setAttribute(sAttr, sNewValue); } return this; }; /** * Adds a class name to the class name collection to be set as a <code>class</code> * attribute when <code>openEnd</code> or <code>voidEnd</code> is called. * * This is only valid when called between <code>openStart/voidStart</code> and <code>openEnd/voidEnd</code>. * * @param {string} sClass Class name to be written * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.class = function(sClass) { if (sClass) { this._sClasses += (this._sClasses) ? " " + sClass : sClass; } return this; }; /** * Adds a style name-value pair to the style collection to be set as a <code>style</code> * attribute when <code>openEnd</code> or <code>voidEnd</code> is called. * * 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} vValue Value of the style property * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.style = function(sName, vValue) { if (!sName || vValue == null || vValue == "") { return this; } vValue = vValue + ""; if (vValue.includes(";")) { // sanitize the semicolon to ensure that a single style rule can be set per style API call oCSSStyleDeclaration.setProperty(sName, vValue); vValue = oCSSStyleDeclaration.getPropertyValue(sName); } this._sStyles += (this._sStyles ? " " : "") + (sName + ": " + vValue + ";"); 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. * * @returns {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.openEnd = function() { if (this._sClasses) { // className can also be an instance of SVGAnimatedString if the element is an SVGElement. Therefore do not use // HTMLElement.className property, it is better to set the classes of an element using HTMLElement.setAttribute. this.attr("class", this._sClasses); this._sClasses = ""; } if (this._sStyles) { // For styles, to be CSP compliant, we use the style property instead of setting the style attribute. // However, using the style property instead of the style attribute might report a mismatch because of // the serialization algorithm of the CSSStyleDeclaration. e.g. // $0.style = "background-color: RED;"; // background-color: red; // $0.style = "background: red;"; // background: red none repeat scroll 0% 0%; // https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block // While it is true that this mismatch might cause a style property call unnecessarily, trying to solve // this problem would not bring a better performance since the possibility of changed styles is much more // less than unchanged styles in the overall rendering. // Therefore, to compare faster, here we do only string-based comparison of retrived and applied styles. // In worst case, we will try to update the style property unnecessarily but this will not be a real // style update for the engine since the parsed CSS declaration blocks will be equal at the end. if (this._mAttributes.style != this._sStyles) { this._oCurrent.style = this._sStyles; } delete this._mAttributes.style; this._sStyles = ""; } if (this._iTagOpenState == 1 /* Tag is Open and Created */) { return this; } for (var sAttribute in this._mAttributes) { var fnMutator = AttributeMutators[sAttribute]; if (!fnMutator || !fnMutator(this._oCurrent, null)) { this._oCurrent.removeAttribute(sAttribute); } delete this._mAttributes[sAttribute]; } 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, the <code>close</code> method must not be called. * * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.voidEnd = function() { this.openEnd(); this._iTagOpenState = 0; /* Closed */ this._insertNewElement(); return this; }; /** * Sets the specified text. * * @param {string} sText Text to be set * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.text = function(sText) { this._walkOnTree(); this._matchNodeName("#text"); if (!this._oCurrent) { this._oCurrent = document.createTextNode(sText); this._oParent.insertBefore(this._oCurrent, this._oReference); } else if (this._oCurrent.data != sText) { this._oCurrent.data = sText; } this._iTagOpenState = 0; /* Closed */ 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 The tag name of the HTML element * @return {this} Reference to <code>this</code> in order to allow method chaining */ Patcher.prototype.close = function(sTagName) { if (this._iTagOpenState) { this._iTagOpenState = 0; /* Closed */ if (this._oCurrent.lastChild) { this._oCurrent.textContent = ""; } } else { var oParent = this._oCurrent.parentNode; for (var oLastChild = oParent.lastChild; oLastChild && oLastChild != this._oCurrent; oLastChild = oParent.lastChild) { oParent.removeChild(oLastChild); } this._oCurrent = oParent; } this._insertNewElement(); return this; }; /** * Replaces the given HTML of the current element being patched. * * <b>Note:</b> This API must not be used to replace the output of the root node. * * @param {string} sHtml HTML markup * @param {sap.ui.core.ID} [sId] ID to identify the element * @param {function} [fnCallback] The callback that can process the inserted DOM nodes after the HTML markup is injected into the DOM tree * @return {this} Reference to <code>this</code> in order to allow method chaining * @SecSink {*|XSS} */ Patcher.prototype.unsafeHtml = function(sHtml, sId, fnCallback) { var oReference = null; var oCurrent = this._oCurrent; if (!oCurrent) { oReference = this._oRoot; } else if (this._iTagOpenState /* Tag is Open */) { oReference = oCurrent.firstChild; if (sHtml) { this._iTagOpenState = 0; /* Tag is Closed */ oCurrent.insertAdjacentHTML("afterbegin", sHtml); this._oCurrent = oReference ? oReference.previousSibling : oCurrent.lastChild; } } else { oReference = oCurrent.nextSibling; if (sHtml) { if (oCurrent.nodeType == 1 /* Node.ELEMENT_NODE */) { oCurrent.insertAdjacentHTML("afterend", sHtml); } else { oTemplateElement.innerHTML = sHtml; oCurrent.parentNode.insertBefore(oTemplateElement.content, oReference); } this._oCurrent = oReference ? oReference.previousSibling : oCurrent.parentNode.lastChild; } } if (sHtml && fnCallback) { var aNodes = [this._oCurrent]; for (var oNode = this._oCurrent.previousSibling; oNode && oNode != oCurrent; oNode = oNode.previousSibling) { aNodes.unshift(oNode); } fnCallback(aNodes); } if (sId && oReference && oReference.id == sId) { oReference.remove(); } return this; }; return Patcher; });