UNPKG

@babylonjs/gui

Version:

Babylon.js GUI module =====================

348 lines 13.9 kB
import { GetClass } from "@babylonjs/core/Misc/typeStore.js"; const XmlLoaderError = "XmlLoader Exception : XML file is malformed or corrupted."; /** * Class used to load GUI via XML. */ export class XmlLoader { /** * Create a new xml loader * @param parentClass Sets the class context. Used when the loader is instanced inside a class and not in a global context */ constructor(parentClass = null) { this._nodes = {}; this._nodeTypes = { element: 1, attribute: 2, text: 3, }; this._isLoaded = false; this._objectAttributes = { textHorizontalAlignment: 1, textVerticalAlignment: 2, horizontalAlignment: 3, verticalAlignment: 4, stretch: 5, }; if (parentClass) { this._parentClass = parentClass; } } _getChainElement(attributeValue) { let element = window; if (this._parentClass) { element = this._parentClass; } let value = attributeValue; value = value.split("."); for (let i = 0; i < value.length; i++) { element = element[value[i]]; } return element; } _getClassAttribute(attributeName) { const attribute = attributeName.split("."); const className = GetClass("BABYLON.GUI." + attribute[0]); return className[attribute[1]]; } _createGuiElement(node, parent, linkParent = true) { try { const className = GetClass("BABYLON.GUI." + node.nodeName); const guiNode = new className(); if (parent && linkParent) { parent.addControl(guiNode); } for (let i = 0; i < node.attributes.length; i++) { if (node.attributes[i].name.toLowerCase().includes("datasource")) { continue; } if (node.attributes[i].name.toLowerCase().includes("observable")) { const element = this._getChainElement(node.attributes[i].value); guiNode[node.attributes[i].name].add(element); continue; } else if (node.attributes[i].name == "linkWithMesh") { if (this._parentClass) { guiNode.linkWithMesh(this._parentClass[node.attributes[i].value]); } else { guiNode.linkWithMesh(window[node.attributes[i].value]); } } else if (node.attributes[i].value.match(/{{.*}}/)) { const matches = node.attributes[i].value.match(/{{(.*)}}/); let element = this._getChainElement(matches[1]); if (!(node.attributes[i].value.startsWith("{{") && node.attributes[i].value.endsWith("}}"))) { element = node.attributes[i].value.replace(/{{.*}}/, `${element}`); } guiNode[node.attributes[i].name] = element; } else if (!this._objectAttributes[node.attributes[i].name]) { if (node.attributes[i].value == "true" || node.attributes[i].value == "false") { guiNode[node.attributes[i].name] = node.attributes[i].value == "true"; } else { guiNode[node.attributes[i].name] = !isNaN(Number(node.attributes[i].value)) ? Number(node.attributes[i].value) : node.attributes[i].value; } } else { guiNode[node.attributes[i].name] = this._getClassAttribute(node.attributes[i].value); } } if (!node.attributes.getNamedItem("id")) { this._nodes[node.nodeName + Object.keys(this._nodes).length + "_gen"] = guiNode; return guiNode; } let id = node.attributes.getNamedItem("id").value; if (id.startsWith("{{") && id.endsWith("}}")) { id = this._getChainElement(id.substring(2, id.length - 2)); } if (!this._nodes[id]) { this._nodes[id] = guiNode; } else { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Duplicate ID, every element should have an unique ID attribute"; } return guiNode; } catch (exception) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Error parsing Control " + node.nodeName + "," + exception + "."; } } _parseGrid(node, guiNode, parent) { let width; let height; let columns; const rows = node.children; let cells; let isPixel = false; let cellNode; let rowNumber = -1; let columnNumber = -1; let totalColumnsNumber = 0; for (let i = 0; i < rows.length; i++) { if (rows[i].nodeType != this._nodeTypes.element) { continue; } if (rows[i].nodeName != "Row") { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Expecting Row node, received " + rows[i].nodeName; } rowNumber += 1; columns = rows[i].children; if (!rows[i].attributes.getNamedItem("height")) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Height must be defined for grid rows"; } height = Number(rows[i].attributes.getNamedItem("height").nodeValue); isPixel = rows[i].attributes.getNamedItem("isPixel") ? JSON.parse(rows[i].attributes.getNamedItem("isPixel").nodeValue) : false; guiNode.addRowDefinition(height, isPixel); for (let j = 0; j < columns.length; j++) { if (columns[j].nodeType != this._nodeTypes.element) { continue; } if (columns[j].nodeName != "Column") { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Expecting Column node, received " + columns[j].nodeName; } columnNumber += 1; if (rowNumber > 0 && columnNumber > totalColumnsNumber) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : In the Grid element, the number of columns is defined in the first row, do not add more columns in the subsequent rows."; } if (rowNumber == 0) { if (!columns[j].attributes.getNamedItem("width")) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Width must be defined for all the grid columns in the first row"; } width = Number(columns[j].attributes.getNamedItem("width").nodeValue); isPixel = columns[j].attributes.getNamedItem("isPixel") ? JSON.parse(columns[j].attributes.getNamedItem("isPixel").nodeValue) : false; guiNode.addColumnDefinition(width, isPixel); } cells = columns[j].children; for (let k = 0; k < cells.length; k++) { if (cells[k].nodeType != this._nodeTypes.element) { continue; } cellNode = this._createGuiElement(cells[k], guiNode, false); guiNode.addControl(cellNode, rowNumber, columnNumber); if (cells[k].firstChild) { this._parseXml(cells[k].firstChild, cellNode); } } } if (rowNumber == 0) { totalColumnsNumber = columnNumber; } columnNumber = -1; } if (node.nextSibling) { this._parseXml(node.nextSibling, parent); } } _parseElement(node, guiNode, parent) { if (node.firstChild) { this._parseXml(node.firstChild, guiNode); } if (node.nextSibling) { this._parseXml(node.nextSibling, parent); } } _prepareSourceElement(node, guiNode, variable, source, iterator) { if (this._parentClass) { this._parentClass[variable] = source[iterator]; } else { window[variable] = source[iterator]; } if (node.firstChild) { this._parseXml(node.firstChild, guiNode, true); } } _parseElementsFromSource(node, guiNode, parent) { const dataSource = node.attributes.getNamedItem("dataSource").value; if (!dataSource.includes(" in ")) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Malformed XML, Data Source must include an in"; } else { let isArray = true; const splittedSource = dataSource.split(" in "); if (splittedSource.length < 2) { // eslint-disable-next-line no-throw-literal throw "XmlLoader Exception : Malformed XML, Data Source must have an iterator and a source"; } let source = splittedSource[1]; if (source.startsWith("{") && source.endsWith("}")) { isArray = false; } if (!isArray || (source.startsWith("[") && source.endsWith("]"))) { source = source.substring(1, source.length - 1); } if (this._parentClass) { source = this._parentClass[source]; } else { source = window[source]; } if (isArray) { for (let i = 0; i < source.length; i++) { this._prepareSourceElement(node, guiNode, splittedSource[0], source, i); } } else { for (const i in source) { this._prepareSourceElement(node, guiNode, splittedSource[0], source, i); } } if (node.nextSibling) { this._parseXml(node.nextSibling, parent); } } } _parseXml(node, parent, generated = false) { if (node.nodeType != this._nodeTypes.element) { if (node.nextSibling) { this._parseXml(node.nextSibling, parent, generated); } return; } if (generated) { node.setAttribute("id", parent.id + (parent._children.length + 1)); } const guiNode = this._createGuiElement(node, parent); if (!this._rootNode) { this._rootNode = guiNode; } if (node.nodeName == "Grid") { this._parseGrid(node, guiNode, parent); } else if (!node.attributes.getNamedItem("dataSource")) { this._parseElement(node, guiNode, parent); } else { this._parseElementsFromSource(node, guiNode, parent); } } /** * Gets if the loading has finished. * @returns whether the loading has finished or not */ isLoaded() { return this._isLoaded; } /** * Gets a loaded node / control by id. * @param id the Controls id set in the xml * @returns element of type Control */ getNodeById(id) { return this._nodes[id]; } /** * Gets all loaded nodes / controls * @returns Array of controls */ getNodes() { return this._nodes; } /** * Disposes the loaded layout */ dispose() { if (this._rootNode) { this._rootNode.dispose(); this._rootNode = null; this._nodes = {}; } } /** * Initiates the xml layout loading * @param xmlFile defines the xml layout to load * @param rootNode defines the node / control to use as a parent for the loaded layout controls. * @param onSuccess defines the callback called on layout load successfully. * @param onError defines the callback called on layout load failure. */ loadLayout(xmlFile, rootNode, onSuccess = null, onError = null) { const xhttp = new XMLHttpRequest(); xhttp.onload = () => { if (xhttp.readyState === 4 && xhttp.status === 200) { if (!xhttp.responseXML) { if (onError) { onError(XmlLoaderError); return; } else { throw XmlLoaderError; } } const xmlDoc = xhttp.responseXML.documentElement; this._parseXml(xmlDoc.firstChild, rootNode); this._isLoaded = true; if (onSuccess) { onSuccess(); } } }; xhttp.onerror = function () { if (onError) { onError("an error occurred during loading the layout"); } }; xhttp.open("GET", xmlFile, true); xhttp.send(); } /** * Initiates the xml layout loading asynchronously * @param xmlFile defines the xml layout to load * @param rootNode defines the node / control to use as a parent for the loaded layout controls. * @returns Promise */ async loadLayoutAsync(xmlFile, rootNode) { return await new Promise((resolve, reject) => { this.loadLayout(xmlFile, rootNode, resolve, reject); }); } } //# sourceMappingURL=xmlLoader.js.map