xmlLoader.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. let id = node.attributes.getNamedItem("id").value;
  86. if (id.startsWith("{{") && id.endsWith("}}")) {
  87. id = this._getChainElement(id.substring(2, id.length - 2));
  88. }
  89. if (!this._nodes[id]) {
  90. this._nodes[id] = guiNode;
  91. } else {
  92. throw "XmlLoader Exception : Duplicate ID, every element should have an unique ID attribute";
  93. }
  94. return guiNode;
  95. } catch (e) {
  96. throw "XmlLoader Exception : Error parsing Control " + node.nodeName + "," + e + ".";
  97. }
  98. }
  99. private _parseGrid(node: any, guiNode: any, parent: any): void {
  100. let width;
  101. let height;
  102. let columns;
  103. let rows = node.children;
  104. let cells;
  105. let isPixel = false;
  106. let cellNode;
  107. let rowNumber = -1;
  108. let columnNumber = -1;
  109. let totalColumnsNumber = 0;
  110. for (let i = 0; i < rows.length; i++) {
  111. if (rows[i].nodeType != this._nodeTypes.element) {
  112. continue;
  113. }
  114. if (rows[i].nodeName != "Row") {
  115. throw "XmlLoader Exception : Expecting Row node, received " + rows[i].nodeName;
  116. }
  117. rowNumber += 1;
  118. columns = rows[i].children;
  119. if (!rows[i].attributes.getNamedItem("height")) {
  120. throw "XmlLoader Exception : Height must be defined for grid rows";
  121. }
  122. height = Number(rows[i].attributes.getNamedItem("height").nodeValue);
  123. isPixel = rows[i].attributes.getNamedItem("isPixel") ? JSON.parse(rows[i].attributes.getNamedItem("isPixel").nodeValue) : false;
  124. guiNode.addRowDefinition(height, isPixel);
  125. for (let j = 0; j < columns.length; j++) {
  126. if (columns[j].nodeType != this._nodeTypes.element) {
  127. continue;
  128. }
  129. if (columns[j].nodeName != "Column") {
  130. throw "XmlLoader Exception : Expecting Column node, received " + columns[j].nodeName;
  131. }
  132. columnNumber += 1;
  133. if (rowNumber > 0 && columnNumber > totalColumnsNumber) {
  134. 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.";
  135. }
  136. if (rowNumber == 0) {
  137. if (!columns[j].attributes.getNamedItem("width")) {
  138. throw "XmlLoader Exception : Width must be defined for all the grid columns in the first row";
  139. }
  140. width = Number(columns[j].attributes.getNamedItem("width").nodeValue);
  141. isPixel = columns[j].attributes.getNamedItem("isPixel") ? JSON.parse(columns[j].attributes.getNamedItem("isPixel").nodeValue) : false;
  142. guiNode.addColumnDefinition(width, isPixel);
  143. }
  144. cells = columns[j].children;
  145. for (let k = 0; k < cells.length; k++) {
  146. if (cells[k].nodeType != this._nodeTypes.element) {
  147. continue;
  148. }
  149. cellNode = this._createGuiElement(cells[k], guiNode, false);
  150. guiNode.addControl(cellNode, rowNumber, columnNumber);
  151. if (cells[k].firstChild) {
  152. this._parseXml(cells[k].firstChild, cellNode);
  153. }
  154. }
  155. }
  156. if (rowNumber == 0) {
  157. totalColumnsNumber = columnNumber;
  158. }
  159. columnNumber = -1;
  160. }
  161. if (node.nextSibling) {
  162. this._parseXml(node.nextSibling, parent);
  163. }
  164. }
  165. private _parseElement(node: any, guiNode: any, parent: any): void {
  166. if (node.firstChild) {
  167. this._parseXml(node.firstChild, guiNode);
  168. }
  169. if (node.nextSibling) {
  170. this._parseXml(node.nextSibling, parent);
  171. }
  172. }
  173. private _prepareSourceElement(node: any, guiNode: any, variable: any, source: any, iterator: any): void {
  174. if (this._parentClass) {
  175. this._parentClass[variable] = source[iterator];
  176. } else {
  177. window[variable] = source[iterator];
  178. }
  179. if (node.firstChild) {
  180. this._parseXml(node.firstChild, guiNode, true);
  181. }
  182. }
  183. private _parseElementsFromSource(node: any, guiNode: any, parent: any): void {
  184. let dataSource = node.attributes.getNamedItem("dataSource").value;
  185. if (!dataSource.includes(" in ")) {
  186. throw "XmlLoader Exception : Malformed XML, Data Source must include an in";
  187. } else {
  188. let isArray = true;
  189. let splittedSource = dataSource.split(" in ");
  190. if (splittedSource.length < 2) {
  191. throw "XmlLoader Exception : Malformed XML, Data Source must an iterator and a source";
  192. }
  193. let source = splittedSource[1];
  194. if (source.startsWith("{") && source.endsWith("}")) {
  195. isArray = false;
  196. }
  197. if (!isArray || (source.startsWith("[") && source.endsWith("]"))) {
  198. source = source.substring(1, source.length - 1);
  199. }
  200. if (this._parentClass) {
  201. source = this._parentClass[source];
  202. } else {
  203. source = window[source];
  204. }
  205. if (isArray) {
  206. for (let i = 0; i < source.length; i++) {
  207. this._prepareSourceElement(node, guiNode, splittedSource[0], source, i);
  208. }
  209. } else {
  210. for (let i in source) {
  211. this._prepareSourceElement(node, guiNode, splittedSource[0], source, i);
  212. }
  213. }
  214. if (node.nextSibling) {
  215. this._parseXml(node.nextSibling, parent);
  216. }
  217. }
  218. }
  219. private _parseXml(node: any, parent: any, generated: boolean = false): void {
  220. if (node.nodeType != this._nodeTypes.element) {
  221. if (node.nextSibling) {
  222. this._parseXml(node.nextSibling, parent, generated);
  223. }
  224. return;
  225. }
  226. if (generated) {
  227. node.setAttribute("id", parent.id + (parent._children.length + 1));
  228. }
  229. let guiNode = this._createGuiElement(node, parent);
  230. if (node.nodeName == "Grid") {
  231. this._parseGrid(node, guiNode, parent);
  232. } else if (!node.attributes.getNamedItem("dataSource")) {
  233. this._parseElement(node, guiNode, parent);
  234. } else {
  235. this._parseElementsFromSource(node, guiNode, parent);
  236. }
  237. }
  238. /**
  239. * Gets if the loading has finished.
  240. * @returns whether the loading has finished or not
  241. */
  242. public isLoaded(): boolean {
  243. return this._isLoaded;
  244. }
  245. /**
  246. * Gets a loaded node / control by id.
  247. * @param id the Controls id set in the xml
  248. * @returns element of type Control
  249. */
  250. public getNodeById(id: string): any {
  251. return this._nodes[id];
  252. }
  253. /**
  254. * Gets all loaded nodes / controls
  255. * @returns Array of controls
  256. */
  257. public getNodes(): any {
  258. return this._nodes;
  259. }
  260. /**
  261. * Initiates the xml layout loading
  262. * @param xmlFile defines the xml layout to load
  263. * @param rootNode defines the node / control to use as a parent for the loaded layout controls.
  264. * @param callback defines the callback called on layout load.
  265. */
  266. public loadLayout(xmlFile: any, rootNode: any, callback: any): void {
  267. let xhttp = new XMLHttpRequest();
  268. xhttp.onreadystatechange = function(this: XmlLoader) {
  269. if (xhttp.readyState == 4 && xhttp.status == 200) {
  270. if (!xhttp.responseXML) {
  271. throw "XmlLoader Exception : XML file is malformed or corrupted.";
  272. }
  273. let xmlDoc = xhttp.responseXML.documentElement;
  274. this._parseXml(xmlDoc.firstChild, rootNode);
  275. this._isLoaded = true;
  276. if (callback) {
  277. callback();
  278. }
  279. }
  280. }.bind(this);
  281. xhttp.open("GET", xmlFile, true);
  282. xhttp.send();
  283. }
  284. }