UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

632 lines (529 loc) 19.5 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ './Template', './TemplateControl', 'sap/ui/thirdparty/handlebars', 'sap/ui/base/ManagedObject', 'sap/base/util/ObjectPath', "sap/base/security/encodeXML", "sap/base/util/isEmptyObject", 'sap/ui/core/Core' // provides sap.ui.getCore() ], function( Template, TemplateControl, Handlebars, ManagedObject, ObjectPath, encodeXML, isEmptyObject ) { "use strict"; /** * Creates and initializes a new handlebars template with the given <code>sId</code> * and settings. * * The set of allowed entries in the <code>mSettings</code> object depends on * the concrete subclass and is described there. * * @param {string} * [sId] optional id for the new template; generated automatically if * no non-empty id is given Note: this can be omitted, no matter * whether <code>mSettings</code> will be given or not! * @param {object} * [mSettings] optional map/JSON-object with initial settings for the * new component instance * @public * * @class The class for Handlebars Templates. * @extends sap.ui.core.tmpl.Template * @abstract * @author SAP SE * @version 1.111.5 * @alias sap.ui.core.tmpl.HandlebarsTemplate * @since 1.15 * @deprecated since 1.56 */ var HandlebarsTemplate = Template.extend("sap.ui.core.tmpl.HandlebarsTemplate", /** @lends sap.ui.core.tmpl.HandlebarsTemplate.prototype */ { metadata: { library: "sap.ui.core" }, constructor : function(sId, mSettings) { Template.apply(this, arguments); } }); // register this template type (as it is the default we do it also in the Template) Template.registerType("text/x-handlebars-template", "sap.ui.core.tmpl.HandlebarsTemplate"); function decodeHash(oHash) { for (var sKey in oHash) { oHash[sKey] = oHash[sKey].replace("&gt;", ">").replace("&lt;", "<").replace("&quot;", "\"").replace("&amp;", "&"); } } function determinePath(sPath, sParentPath) { // split given paths in model name (optional) and path var rPath = /^((\w+)>)?(.*)/, aPathParts = rPath.exec(sPath), sPathModel = aPathParts[2], aParentPathParts = rPath.exec(sParentPath), sParentPathModel = aParentPathParts[2]; var aPathParts = rPath.exec(sPath); // if the model matches and the path is not absolute they need to be concatenated if (sParentPath && sPathModel == sParentPathModel) { return sParentPath + aPathParts[3]; } else { return sPath; } } /** * Handlebars helpers for the rendering phase! * @private */ HandlebarsTemplate.RENDER_HELPERS = (function() { // TODO: find a point in time for destroying this RenderManager again // extended helpers: // - each // - if // - unless // - with (TODO) // custom helpers: // - control: allows to declare a UI5 control // - element // - attribute // - property // - aggregation // define the options to render the properties, aggregations, events, ... var fnEach = Handlebars.helpers["each"], fnWith = Handlebars.helpers["with"], fnIf = Handlebars.helpers["if"], fnUnless = Handlebars.helpers["unless"], oRenderManager = sap.ui.getCore().createRenderManager(); // this special RenderManager is used to write the controlData, classes // and styles into the buffer and extract it later on via getHTML! oRenderManager.renderControl = function(oControl) { this.writeControlData(oControl); this.writeClasses(oControl); this.writeStyles(oControl); }; var oHelpers = { "each": function(context, options) { options = options || context; if (!options.hash.path) { // call the original function return fnEach.apply(this, arguments); } else { // make sure to decode the hash entries decodeHash(options.hash); // parse the path & find the model var oRM = options.data.renderManager, oRootControl = options.data.rootControl, sParentPath = options.data.path, oParentControl = options.data.parentControl, sPath = determinePath(options.hash.path, sParentPath), oProperty = oRootControl.bindList(sPath), aHTML = [], data; // frame the data (isolation) if (options.data) { data = Handlebars.createFrame(options.data); } // iterate through the entries of the property if (oProperty) { for (var sKey in oProperty) { //var oValue = oProperty[sKey]; if (data) { data.renderManager = oRM; data.rootControl = oRootControl; data.path = sPath + "/" + sKey + "/"; data.parentControl = oParentControl; } // pass the current value as context to allow nested // usage of the standard placeholders //aHTML.push(options.fn(oValue, { data: data })); // we do not pass a context since the expressions for UI5 are // based on the models aHTML.push(options.fn({}, { data: data })); } } // let's return the markup if (!oParentControl) { return new Handlebars.SafeString(aHTML.join("")); } } }, "with": function(context, options) { options = options || context; if (!options.hash.path) { // call the original function return fnWith.apply(this, arguments); } }, "if": function(conditional, options) { options = options || conditional; if (!options.hash.path) { // call the original function return fnIf.apply(this, arguments); } else { // make sure to decode the hash entries decodeHash(options.hash); // lookup the required infos var oRootControl = options.data.rootControl, sParentPath = options.data.path, sPath = determinePath(options.hash.path, sParentPath); // only in case of a path is specified the handler can work if (sPath) { // bind and returns true/false dependent on the value var oValue = oRootControl.bindProp(sPath); if (oValue) { return options.fn(this); } else { return options.inverse(this); } } } }, "unless": function(conditional, options) { options = options || conditional; if (!options.hash.path) { // call the original function return fnUnless.apply(this, arguments); } else { // make sure to decode the hash entries decodeHash(options.hash); // lookup the required infos var oRootControl = options.data.rootControl, sParentPath = options.data.path, sPath = determinePath(options.hash.path, sParentPath); // only in case of a path is specified the handler can work if (sPath) { // bind and returns true/false dependent on the value var oValue = oRootControl.bindProp(sPath); if (!oValue) { return options.fn(this); } else { return options.inverse(this); } } } }, "text": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // lookup the required infos var oRootControl = options.data.rootControl, sParentPath = options.data.path, sPath = determinePath(options.hash.path, sParentPath); // only in case of a path is specified the handler can work if (sPath) { // bind and return the text var oValue = oRootControl.bindProp(sPath); return oValue && new Handlebars.SafeString(encodeXML(oValue)); } else { throw new Error("The expression \"text\" requires the option \"path\"!"); } }, "element": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // create and return the DOM element var oRM = options.data.renderManager, oRootControl = options.data.rootControl, oElement = oRootControl.createDOMElement(options.hash, options.data.path), oParentElement = options.data.parentElement; // Example for defining nested elements: // {{#element ...}} // {{element ...}} <-- nested element // {{/element}} if (options.fn) { options.fn({}, { data: { renderManager: oRM, rootControl: oRootControl, parentElement: oElement } }); } // in case of having a parent DOM element, we add the DOM element to // the parent and do not return an HTML string if (oParentElement) { oParentElement.addElement(oElement); return; } // create the HTML markup and return it return new Handlebars.SafeString(oRM.getHTML(oElement)); }, "control": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // extract the data information var oRM = options.data.renderManager, oControl = options.data.control; // aggregation support to render the control only (to support markup) // e.g.: // {{#aggregation name="content" type="sap.ui.core.Control" multiple="true"}} // <div> // {{control}} <== place where to render the control! // </div> // {{/aggregation}} if (oControl) { return new Handlebars.SafeString(oRM.getHTML(oControl)); } // extract the rest of the information which is required now var oRootControl = options.data.rootControl, sParentPath = options.data.path, mParentChildren = options.data.children, sType = options.hash["sap-ui-type"], oClass = ObjectPath.get(sType || ""), oMetadata = oClass && oClass.getMetadata(), sDefaultAggregation = options.hash["sap-ui-default-aggregation"] || oMetadata && oMetadata.getDefaultAggregationName(), oView = options.data.view; // throw error if control cannot be found if (!oClass) { throw new Error("Control of type " + sType + " cannot be found."); } // Nested controls will get the reference to the parent control in order // to add them to the defined aggregation. Example of nested controls: // {{#control ...}} // {{control ...}} <-- nested control // {{/control}} var mChildren = {}; if (options.fn) { options.fn({}, { data: { rootControl: oRootControl, path: sParentPath, children: mChildren, defaultAggregation: sDefaultAggregation, view: oView } }); } // remove the found nested children from the mSettings because they will // be handled after the creation of the new control instance var mSettings = Object.assign({}, options.hash), aStyleClasses; for (var sKey in mSettings) { //var oValue = mSettings[sKey]; if (sKey === "sap-ui-class" && typeof mSettings[sKey] === "string") { aStyleClasses = mSettings["sap-ui-class"] && mSettings["sap-ui-class"].split(" "); delete mSettings[sKey]; } else if (mChildren[sKey]) { delete mSettings[sKey]; } } // create the new control (out of the hash information) var oNewControl = oRootControl.createControl(mSettings, options.data.path, !!mParentChildren, oView); if (aStyleClasses && aStyleClasses.length > 0) { aStyleClasses.forEach(oNewControl.addStyleClass.bind(oNewControl)); } // add the created children to current control instance either as template // in case of a binding has been found or as aggregation in case of no // binding was found if (!isEmptyObject(mChildren)) { mSettings = options.hash; var oAllAggregation = oMetadata.getAllAggregations(); for (var sAggregationName in mChildren) { var aChildAggregation = mChildren[sAggregationName]; for (var i = 0, l = aChildAggregation.length; i < l; i++) { var oChildControl = aChildAggregation[i], oAggregation = oAllAggregation[sAggregationName], bMultiple = oAggregation && oAggregation.multiple; if (typeof mSettings[sAggregationName] === "string") { // the aggregation is bound => so we create a binding info object // which is used in the createControl function of the TemplateControl // to create a proper binding var oBindingInfo = ManagedObject.bindingParser(mSettings[sAggregationName], oView && oView.getController()); oBindingInfo.template = oChildControl; oNewControl.bindAggregation(sAggregationName, oBindingInfo); } else { // the aggregation is not bound => so we add nested controls to the aggregation if (bMultiple) { oNewControl.addAggregation(sAggregationName, oChildControl); } else { oNewControl.setAggregation(sAggregationName, oChildControl); } } } } } // if we find a parent children map the control will not return // the markup - furthermore the control will be added to the parent // control in the section above into the desired aggregation if (mParentChildren) { var sAggregationName = options.hash["sap-ui-aggregation"] || options.data.defaultAggregation; mParentChildren[sAggregationName] = mParentChildren[sAggregationName] || []; mParentChildren[sAggregationName].push(oNewControl); return; } // in case of the root control we return the markup return new Handlebars.SafeString(oRM.getHTML(oNewControl)); }, "property": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // use the getter to access the property var oRootControl = options.data.rootControl, oMetadata = oRootControl.getMetadata(), sPropertyName = options.hash.name, sGetter = oMetadata.getProperty(sPropertyName)._sGetter; return oRootControl[sGetter](); }, "aggregation": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // when data provides the children object we are running in // the use case to be used as kind of scope for the aggregation // to be used and when running without children object the // aggregation helper is used for defining an aggregation // of a new control type being declared as template if (options.data.children) { // extract the required info var sAggregationName = options.hash.name; // By defining the aggregation helper the default aggregation // of nested controls can be adopted // {{#aggregation ...}} // {{control ...}} <-- nested control // {{/aggregation}} if (options.fn) { var oData = Object.assign({}, options.data, { defaultAggregation: sAggregationName }); options.fn({}, { data: oData }); } } else { // extract the required info var oRM = options.data.renderManager, oRootControl = options.data.rootControl, oMetadata = oRootControl.getMetadata(), sAggregationName = options.hash.name, sGetter = oMetadata.getAggregation(sAggregationName)._sGetter, aHTML = []; // retrieve the child elements via the specific getter // and create the markup for the nested elements var aChildren = oRootControl[sGetter](); if (aChildren) { for (var i = 0, l = aChildren.length; i < l; i++) { // if the aggregation contains nested content => execute it! if (options.fn) { aHTML.push(options.fn({}, { data: { renderManager: oRM, rootControl: oRootControl, control: aChildren[i] } })); } else { // simply render the control aHTML.push(oRM.getHTML(aChildren[i])); } } } // return the markup return new Handlebars.SafeString(aHTML.join("")); } }, "event": function(context, options) { options = options || context; }, "controlData": function(context, options) { options = options || context; // extract the required info var oRootControl = options.data.rootControl; // return the markup return new Handlebars.SafeString(oRenderManager.getHTML(oRootControl)); } }; return oHelpers; }()); HandlebarsTemplate.prototype.createMetadata = function() { // compile the template // (TODO - think about avoid to compile the template multiple times) var sTemplate = this.getContent(), fnTemplate = this._fnTemplate = this._fnTemplate || Handlebars.compile(sTemplate); // identify the control metadata: properties, aggregations, ... // the template will be executed with specific options var oMetadata = {}, mJSONKeys = TemplateControl.getMetadata().getAllSettings(), mPrivateAggregations = TemplateControl.getMetadata().getAllPrivateAggregations(); // the options to identify the properties, aggregations, events, ... var oHelpers = { "property": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // identify the property and register non standard properties (anything else than id, style, class) var sName = options.hash.name; if (sName && sName !== "id" && !mJSONKeys[sName]) { oMetadata.properties = oMetadata.properties || {}; oMetadata.properties[sName] = Object.assign({}, {type: "string"}, options.hash); } else { throw new Error("The property name \"" + sName + "\" is reserved."); } }, "aggregation": function(context, options) { options = options || context; // make sure to decode the hash entries decodeHash(options.hash); // identify the aggregations and register non standard aggregations (anything else than tooltip, customData, layoutData) var sName = options.hash.name; if (sName && !mJSONKeys[sName] && !mPrivateAggregations[sName]) { options.hash.multiple = options.hash.multiple == "true"; // type correction oMetadata.aggregations = oMetadata.aggregations || {}; oMetadata.aggregations[sName] = Object.assign({}, options.hash); } else { throw new Error("The aggregation name \"" + sName + "\" is reserved."); } }, "event": function(context, options) { options = options || context; }, "controlData": function(context, options) { options = options || context; oMetadata._hasControlData = true; } }; // ignore the following block helper ["each", "if", "unless", "with"].forEach(function(sValue) { oHelpers[sValue] = function() {}; }); // execute the templates with the above options fnTemplate({}, { helpers: oHelpers }); // return the new control metadata return oMetadata; }; HandlebarsTemplate.prototype.createRenderer = function(oView) { // compile the template // (TODO - think about avoid to compile the template multiple times) var sTemplate = this.getContent(), fnTemplate = this._fnTemplate = this._fnTemplate || Handlebars.compile(sTemplate); // create the renderer for the control var fnRenderer = function(rm, oControl) { // execute the template with the above options var sHTML = fnTemplate(oControl.getContext() || {}, { data: { renderManager: rm, rootControl: oControl, view: oView }, helpers: HandlebarsTemplate.RENDER_HELPERS }); // write the markup rm.write(sHTML); }; return fnRenderer; }; return HandlebarsTemplate; });