UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

315 lines (283 loc) 12.2 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.ui.core.HTML. sap.ui.define([ 'sap/ui/thirdparty/jquery', "sap/base/Log", './Control', './RenderManager', "./HTMLRenderer", "sap/base/security/sanitizeHTML" ], function(jQuery, Log, Control, RenderManager, HTMLRenderer, sanitizeHTML) { "use strict"; // local shortcut var RenderPrefixes = RenderManager.RenderPrefixes; /** * Constructor for a new HTML. * * @param {string} [sId] id for the new control, generated automatically if no id is given * @param {object} [mSettings] initial settings for the new control * * @class * Embeds standard HTML in an SAPUI5 control tree. * * Security Hint: By default, the HTML content (property 'content') is not sanitized and therefore * open to XSS attacks. Applications that want to show user defined input in an HTML control, should * either sanitize the content on their own or activate automatic sanitizing through the * {@link #setSanitizeContent sanitizeContent} property. * * Although this control inherits the <code>tooltip</code> aggregation/property and the * <code>hasStyleClass</code>, <code>addStyleClass</code>, <code>removeStyleClass</code> and * <code>toggleStyleClass</code> methods from its base class, it doesn't support them. * Instead, the defined HTML content can contain a tooltip (title attribute) or custom CSS classes. * * For further hints about usage restrictions for this control, see also the documentation of the * <code>content</code> property. * * @extends sap.ui.core.Control * * @author SAP SE * @version 1.147.0 * * @public * @alias sap.ui.core.HTML */ var HTML = Control.extend("sap.ui.core.HTML", /** @lends sap.ui.core.HTML.prototype */ { metadata : { library : "sap.ui.core", properties : { /** * HTML content to be displayed, defined as a string. * * The content is converted to DOM nodes with a call to <code>new jQuery(content)</code>, so any * restrictions for the jQuery constructor apply to the content of the HTML control as well. * * Some of these restrictions (there might be others!) are: * <ul> * <li>the content must be enclosed in tags, pure text is not supported. </li> * <li>if the content contains script tags, they will be executed but they will not appear in the * resulting DOM tree. When the contained code tries to find the corresponding script tag, * it will fail.</li> * <li>if the control is used in an {@link sap/ui/core/mvc/XMLView XMLView}, ensure proper escaping as described * in {@link topic:5ee3be4727864bb08b991414e0428e38 Control Properties and Associations in XML Views}</li> * </ul> * * Please consider to consult the jQuery documentation as well. * * The HTML control currently doesn't prevent the usage of multiple root nodes in its DOM content * (e.g. <code>setContent("&lt;div>&lt;/div>&lt;div>&lt;/div>")</code>), but this is not a guaranteed feature. * The accepted content might be restricted to single root nodes in future versions. * To notify applications about this fact, a warning is written in the log when multiple root nodes are used. * * When changing the content dynamically, ensure that the ID of the root node remains the same as the HTML * control's ID. Otherwise it cannot be guaranteed that certain lifecycle events take place. * * @SecSink {,XSS} The content of the 'content' property is rendered 1:1 to allow the full * flexibility of HTML in UI5 applications. Applications therefore must ensure, that they don't * set malicious content (e.g. derived from user input). UI5 does not provide an HTML validation * function. {@link sap/base/security/encodeXML encodeXML} will encode any HTML relevant character, * but this is in nearly all cases not what applications want here. */ content : {type : "string", group : "Misc", defaultValue : null}, /** * Whether existing DOM content is preferred over the given content string. * * There are two scenarios where this flag is relevant (when set to true): * <ul> * <li>for the initial rendering: when an HTML control is added to a UIArea for the first time * and if the root node of that UIArea contained DOM content with the same id as the HTML * control, then that content will be used for rendering instead of any specified string * content</li> * <li>any follow-up rendering: when an HTML control is rendered for the second or any later * time and the preferDOM flag is set, then the DOM from the first rendering is preserved * and not replaced by the string content</li> * </ul> * * As preserving the existing DOM is the most common use case of the HTML control, the default value is true. */ preferDOM : {type : "boolean", group : "Misc", defaultValue : true}, /** * Whether to run the HTML sanitizer once the content (HTML markup) is applied or not. * * To configure the set of allowed URLs, you can use the {@link module:sap/base/security/URLListValidator.add URLListValidator API}. */ sanitizeContent : {type : "boolean", group : "Misc", defaultValue : false}, /** * Specifies whether the control is visible. Invisible controls are not rendered. */ visible : {type : "boolean", group : "Appearance", defaultValue : true} }, events : { /** * Fired after the HTML control has been rendered. Allows to manipulate the resulting DOM. * * When the control doesn't have string content and no preserved DOM existed for this control, * then this event will fire, but there won't be a DOM node for this control. */ afterRendering : { parameters : { /** * Whether the current DOM of the control has been preserved (true) or not (e.g. * rendered from content property or it is an empty HTML control). */ isPreservedDOM : {type : "boolean"} } } } }, renderer: HTMLRenderer }); /** * @param {string} [sSuffix=''] Suffix of the Element to be retrieved or empty * @returns {Element|null} The element's DOM reference or <code>null</code> * @public */ HTML.prototype.getDomRef = function(sSuffix) { var sId = sSuffix ? this.getId() + "-" + sSuffix : this.getId(); return document.getElementById(RenderPrefixes.Dummy + sId) || document.getElementById(sId); }; HTML.prototype.setContent = function(sContent) { function parseHTML(s) { if ( jQuery.parseHTML ) { var a = jQuery.parseHTML(s); if ( a ) { var start = 0, end = a.length; while ( start < end && a[start].nodeType != 1 ) { start++; } while ( start < end && a[end - 1].nodeType != 1 ) { end--; } if ( start > 0 || end < a.length ) { a = a.slice(start, end); } return jQuery(a); } } return jQuery(s); } if ( this.getSanitizeContent() ) { Log.trace("sanitizing HTML content for " + this); sContent = sanitizeHTML(sContent); } this.setProperty("content", sContent, true); if ( /* sContent && */ this.getDomRef() ) { var $newContent = parseHTML(this.getContent()); // TODO what if content is not HTML (e.g. #something)? jQuery(this.getDomRef()).replaceWith($newContent); this._postprocessNewContent($newContent); } else { this.invalidate(); } return this; }; HTML.prototype.setSanitizeContent = function(bSanitizeContent) { this.setProperty("sanitizeContent", bSanitizeContent, true); // if sanitizeContent has been enabled, set the content again to enable sanitizing on current content if (bSanitizeContent) { this.setContent(this.getContent()); } return this; }; HTML.prototype.onBeforeRendering = function() { if (!this.getPreferDOM()) { return; } var oDomRef = this.getDomRef(); if (oDomRef && !RenderManager.isPreservedContent(oDomRef)) { // before the re-rendering move all "to-be-preserved" nodes to the preserved area for (var sId = oDomRef.id, oNextDomRef; oDomRef && oDomRef.getAttribute("data-sap-ui-preserve") == sId; oDomRef = oNextDomRef) { oNextDomRef = oDomRef.nextElementSibling; RenderManager.preserveContent(oDomRef, /* bPreserveRoot */ true, /* bPreserveNodesWithId */ false); } } }; /** * If the HTML doesn't contain own content, it tries to reproduce existing content */ HTML.prototype.onAfterRendering = function() { if (!this.getVisible()) { // Just leave the placeholder there return; } var $placeholder = jQuery(document.getElementById(RenderPrefixes.Dummy + this.getId())); var $oldContent = RenderManager.findPreservedContent(this.getId()); var $newContent; var isPreservedDOM = false; if ( /*this.getContent() && */ (!this.getPreferDOM() || $oldContent.length == 0) ) { // remove old, preserved content $oldContent.remove(); // replace placeholder with content string $newContent = new jQuery(this.getContent()); // TODO what if content is not HTML (e.g. #something)? $placeholder.replaceWith($newContent); } else if ( $oldContent.length > 0 ) { // replace dummy with old content $placeholder.replaceWith($oldContent); $newContent = $oldContent; isPreservedDOM = true; } else { $placeholder.remove(); } this._postprocessNewContent($newContent); this.fireAfterRendering({isPreservedDOM : isPreservedDOM}); }; HTML.prototype._postprocessNewContent = function($newContent) { if ( $newContent && $newContent.length > 0 ) { if ( $newContent.length > 1 ) { Log.warning("[Unsupported Feature]: " + this + " has rendered " + $newContent.length + " root nodes!"); } else { var sContentId = $newContent.attr("id"); if (sContentId && sContentId != this.getId()) { Log.warning("[Unsupported Feature]: Id of HTML Control '" + this.getId() + "' does not match with content id '" + sContentId + "'!"); } } if (this.getPreferDOM()) { // set a marker that identifies all root nodes in $newContent as 'to-be-preserved' RenderManager.markPreservableContent($newContent, this.getId()); } // and if no node has the control id, search the first without an id and set it if ( $newContent.find("#" + this.getId().replace(/(:|\.)/g,'\\$1')).length === 0 ) { $newContent.filter(":not([id])").first().attr("id", this.getId()); } } else { Log.debug("" + this + " is empty after rendering, setting bOutput to false"); this.bOutput = false; // clean up internal rendering bookkeeping } }; /** * Sets some new DOM content for this HTML control. The content will replace the existing content * after the next rendering. Properties are not modified, but preferDOM should be set to true. * * @param {Element} oDom the new DOM content * @return {this} <code>this</code> to facilitate method chaining * @public */ HTML.prototype.setDOMContent = function(oDom) { var $newContent = jQuery(oDom); if ( this.getDomRef() ) { jQuery(this.getDomRef()).replaceWith($newContent); this._postprocessNewContent($newContent); } else { $newContent.appendTo(RenderManager.getPreserveAreaRef()); if ( this.getUIArea() ) { this.getUIArea().invalidate(); } // TODO fix issue with Control.rerender() this._postprocessNewContent($newContent); // CHECK is it okay to set bOutput to false for empty content? } return this; }; HTML.prototype.setTooltip = function() { Log.warning("The sap.ui.core.HTML control doesn't support tooltips. Add the tooltip to the HTML content instead."); return Control.prototype.setTooltip.apply(this, arguments); }; "hasStyleClass addStyleClass removeStyleClass toggleStyleClass".split(" ").forEach(function(method) { HTML.prototype[method] = function() { Log.warning("The sap.ui.core.HTML control doesn't support custom style classes. Manage custom CSS classes in the HTML content instead."); return Control.prototype[method].apply(this, arguments); }; }); return HTML; });