xmlLoader.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { _TypeStore } from 'babylonjs/Misc/typeStore';
  2. /**
  3. * Class used to load GUI via XML.
  4. */
  5. export class XmlLoader {
  6. private _nodes: any = {};
  7. private _nodeTypes: any = {
  8. element: 1,
  9. attribute: 2,
  10. text: 3
  11. };
  12. private _isLoaded: boolean = false;
  13. private _objectAttributes: any = {
  14. "textHorizontalAlignment": 1,
  15. "textVerticalAlignment": 2,
  16. "horizontalAlignment": 3,
  17. "verticalAlignment": 4,
  18. "stretch": 5,
  19. };
  20. private _parentClass: any;
  21. /**
  22. * Create a new xml loader
  23. * @param parentClass Sets the class context. Used when the loader is instanced inside a class and not in a global context
  24. */
  25. constructor(parentClass = null) {
  26. if (parentClass) {
  27. this._parentClass = parentClass;
  28. }
  29. }
  30. private _getChainElement(attributeValue: any): any {
  31. let element: any = window;
  32. if (this._parentClass) {
  33. element = this._parentClass;
  34. }
  35. let value = attributeValue;
  36. value = value.split(".");
  37. for (let i = 0; i < value.length; i++) {
  38. element = element[value[i]];
  39. }
  40. return element;
  41. }
  42. private _getClassAttribute(attributeName : string) : any {
  43. const attribute = attributeName.split(".");
  44. const className = _TypeStore.GetClass("BABYLON.GUI." + attribute[0]);
  45. return className[attribute[1]];
  46. }
  47. private _createGuiElement(node: any, parent: any, linkParent: boolean = true): void {
  48. try {
  49. let className = _TypeStore.GetClass("BABYLON.GUI." + node.nodeName);
  50. let guiNode = new className();
  51. if (parent && linkParent) {
  52. parent.addControl(guiNode);
  53. }
  54. for (let i = 0; i < node.attributes.length; i++) {
  55. if (node.attributes[i].name.toLowerCase().includes("datasource")) {
  56. continue;
  57. }
  58. if (node.attributes[i].name.toLowerCase().includes("observable")) {
  59. let element = this._getChainElement(node.attributes[i].value);
  60. guiNode[node.attributes[i].name].add(element);
  61. continue;
  62. } else if (node.attributes[i].name == "linkWithMesh") {
  63. if (this._parentClass) {
  64. guiNode.linkWithMesh(this._parentClass[node.attributes[i].value]);
  65. } else {
  66. guiNode.linkWithMesh(window[node.attributes[i].value]);
  67. }
  68. } else if (node.attributes[i].value.startsWith("{{") && node.attributes[i].value.endsWith("}}")) {
  69. let element = this._getChainElement(node.attributes[i].value.substring(2, node.attributes[i].value.length - 2));
  70. guiNode[node.attributes[i].name] = element;
  71. } else if (!this._objectAttributes[node.attributes[i].name]) {
  72. if (node.attributes[i].value == "true" || node.attributes[i].value == "false") {
  73. guiNode[node.attributes[i].name] = (node.attributes[i].value == 'true');
  74. } else {
  75. guiNode[node.attributes[i].name] = !isNaN(Number(node.attributes[i].value)) ? Number(node.attributes[i].value) : node.attributes[i].value;
  76. }
  77. } else {
  78. guiNode[node.attributes[i].name] = this._getClassAttribute(node.attributes[i].value);
  79. }
  80. }
  81. if (!node.attributes.getNamedItem("id")) {
  82. this._nodes[node.nodeName + Object.keys(this._nodes).length + "_gen"] = guiNode;
  83. return guiNode;
  84. }
  85. if (!this._nodes[node.attributes.getNamedItem("id").nodeValue]) {
  86. this._nodes[node.attributes.getNamedItem("id").nodeValue] = guiNode;
  87. } else {
  88. throw "XmlLoader Exception : Duplicate ID, every element should have an unique ID attribute";
  89. }
  90. return guiNode;
  91. } catch (e) {
  92. throw "XmlLoader Exception : Error parsing Control " + node.nodeName + "," + e + ".";
  93. }
  94. }
  95. private _parseGrid(node: any, guiNode: any, parent: any): void {
  96. let width;
  97. let height;
  98. let columns;
  99. let rows = node.children;
  100. let cells;
  101. let isPixel = false;
  102. let cellNode;
  103. let rowNumber = -1;
  104. let columnNumber = -1;
  105. let totalColumnsNumber = 0;
  106. for (let i = 0; i < rows.length; i++) {
  107. if (rows[i].nodeType != this._nodeTypes.element) {
  108. continue;
  109. }
  110. if (rows[i].nodeName != "Row") {
  111. throw "XmlLoader Exception : Expecting Row node, received " + rows[i].nodeName;
  112. }
  113. rowNumber += 1;
  114. columns = rows[i].children;
  115. if (!rows[i].attributes.getNamedItem("height")) {
  116. throw "XmlLoader Exception : Height must be defined for grid rows";
  117. }
  118. height = Number(rows[i].attributes.getNamedItem("height").nodeValue);
  119. isPixel = rows[i].attributes.getNamedItem("isPixel") ? JSON.parse(rows[i].attributes.getNamedItem("isPixel").nodeValue) : false;
  120. guiNode.addRowDefinition(height, isPixel);
  121. for (let j = 0; j < columns.length; j++) {
  122. if (columns[j].nodeType != this._nodeTypes.element) {
  123. continue;
  124. }
  125. if (columns[j].nodeName != "Column") {
  126. throw "XmlLoader Exception : Expecting Column node, received " + columns[j].nodeName;
  127. }
  128. columnNumber += 1;
  129. if (rowNumber > 0 && columnNumber > totalColumnsNumber) {
  130. 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.";
  131. }
  132. if (rowNumber == 0) {
  133. if (!columns[j].attributes.getNamedItem("width")) {
  134. throw "XmlLoader Exception : Width must be defined for all the grid columns in the first row";
  135. }
  136. width = Number(columns[j].attributes.getNamedItem("width").nodeValue);
  137. isPixel = columns[j].attributes.getNamedItem("isPixel") ? JSON.parse(columns[j].attributes.getNamedItem("isPixel").nodeValue) : false;
  138. guiNode.addColumnDefinition(width, isPixel);
  139. }
  140. cells = columns[j].children;
  141. for (let k = 0; k < cells.length; k++) {
  142. if (cells[k].nodeType != this._nodeTypes.element) {
  143. continue;
  144. }
  145. cellNode = this._createGuiElement(cells[k], guiNode, false);
  146. guiNode.addControl(cellNode, rowNumber, columnNumber);
  147. if (cells[k].firstChild) {
  148. this._parseXml(cells[k].firstChild, cellNode);
  149. }
  150. }
  151. }
  152. if (rowNumber == 0) {
  153. totalColumnsNumber = columnNumber;
  154. }
  155. columnNumber = -1;
  156. }
  157. if (node.nextSibling) {
  158. this._parseXml(node.nextSibling, parent);
  159. }
  160. }
  161. private _parseElement(node: any, guiNode: any, parent: any): void {
  162. if (node.firstChild) {
  163. this._parseXml(node.firstChild, guiNode);
  164. }
  165. if (node.nextSibling) {
  166. this._parseXml(node.nextSibling, parent);
  167. }
  168. }
  169. private _prepareSourceElement(node: any, guiNode: any, variable: any, source: any, iterator: any): void {
  170. if (this._parentClass) {
  171. this._parentClass[variable] = source[iterator];
  172. } else {
  173. window[variable] = source[iterator];
  174. }
  175. if (node.firstChild) {
  176. this._parseXml(node.firstChild, guiNode, true);
  177. }
  178. }
  179. private _parseElementsFromSource(node: any, guiNode: any, parent: any): void {
  180. let dataSource = node.attributes.getNamedItem("dataSource").value;
  181. if (!dataSource.includes(" in ")) {
  182. throw "XmlLoader Exception : Malformed XML, Data Source must include an in";
  183. } else {
  184. let isArray = true;
  185. let splittedSource = dataSource.split(" in ");
  186. if (splittedSource.length < 2) {
  187. throw "XmlLoader Exception : Malformed XML, Data Source must an iterator and a source";
  188. }
  189. let source = splittedSource[1];
  190. if (source.startsWith("{") && source.endsWith("}")) {
  191. isArray = false;
  192. }
  193. if (!isArray || (source.startsWith("[") && source.endsWith("]"))) {
  194. source = source.substring(1, source.length - 1);
  195. }
  196. if (this._parentClass) {
  197. source = this._parentClass[source];
  198. } else {
  199. source = window[source];
  200. }
  201. if (isArray) {
  202. for (let i = 0; i < source.length; i++) {
  203. this._prepareSourceElement(node, guiNode, splittedSource[0], source, i);
  204. }
  205. } else {
  206. for (let i in source) {
  207. this._prepareSourceElement(node, guiNode, splittedSource[0], source, i);
  208. }
  209. }
  210. if (node.nextSibling) {
  211. this._parseXml(node.nextSibling, parent);
  212. }
  213. }
  214. }
  215. private _parseXml(node: any, parent: any, generated: boolean = false): void {
  216. if (node.nodeType != this._nodeTypes.element) {
  217. if (node.nextSibling) {
  218. this._parseXml(node.nextSibling, parent, generated);
  219. }
  220. return;
  221. }
  222. if (generated) {
  223. node.setAttribute("id", parent.id + parent._children.length + 1);
  224. }
  225. let guiNode = this._createGuiElement(node, parent);
  226. if (node.nodeName == "Grid") {
  227. this._parseGrid(node, guiNode, parent);
  228. } else if (!node.attributes.getNamedItem("dataSource")) {
  229. this._parseElement(node, guiNode, parent);
  230. } else {
  231. this._parseElementsFromSource(node, guiNode, parent);
  232. }
  233. }
  234. /**
  235. * Gets if the loading has finished.
  236. * @returns whether the loading has finished or not
  237. */
  238. public isLoaded(): boolean {
  239. return this._isLoaded;
  240. }
  241. /**
  242. * Gets a loaded node / control by id.
  243. * @param id the Controls id set in the xml
  244. * @returns element of type Control
  245. */
  246. public getNodeById(id: string): any {
  247. return this._nodes[id];
  248. }
  249. /**
  250. * Gets all loaded nodes / controls
  251. * @returns Array of controls
  252. */
  253. public getNodes(): any {
  254. return this._nodes;
  255. }
  256. /**
  257. * Initiates the xml layout loading
  258. * @param xmlFile defines the xml layout to load
  259. * @param rootNode defines the node / control to use as a parent for the loaded layout controls.
  260. * @param callback defines the callback called on layout load.
  261. */
  262. public loadLayout(xmlFile: any, rootNode: any, callback: any): void {
  263. let xhttp = new XMLHttpRequest();
  264. xhttp.onreadystatechange = function(this: XmlLoader) {
  265. if (xhttp.readyState == 4 && xhttp.status == 200) {
  266. if (!xhttp.responseXML) {
  267. throw "XmlLoader Exception : XML file is malformed or corrupted.";
  268. }
  269. let xmlDoc = xhttp.responseXML.documentElement;
  270. this._parseXml(xmlDoc.firstChild, rootNode);
  271. this._isLoaded = true;
  272. if (callback) {
  273. callback();
  274. }
  275. }
  276. }.bind(this);
  277. xhttp.open("GET", xmlFile, true);
  278. xhttp.send();
  279. }
  280. }