Source: renderers/VirtualDOMRenderer.js

import { Shark } from "../Shark";
import Mustache from "mustache";
import $ from "jquery";
import SmartModel from "../SmartModel";
import { Utils } from "../Utils";

import parser from "html2hscript";
import h from "virtual-dom/h";
import diff from 'virtual-dom/diff';
import patch from 'virtual-dom/patch';
import createElement from 'virtual-dom/create-element';


/**
 * Is the bridge between the Shark main class and VirtualDOM.
 * @constructor
 */

class VirtualDOMRenderer {
	constructor () {
		if (typeof Mustache === "undefined") {
			throw (new Error("Mustache is needed to use VirtualDOMRenderer!"));
		}

		this.rootNode = null;
		this.tree = null;
	}

	/**
	 * Takes a Jaw, get the template from Shark, put the dataObject from the Jaw into the template and add the rendered data into the container
	 * passed in the options.
	 *
	 * @param {Jaw} jaw - The view to render.
	 * @param {object} options - The object with the options to setup the render.
	 * @param {string} options.container - The CSS selector to use to insert the rendered content. To place the content is used jQuery.html() method.
	 */
	render (jaw, options) {
		return new Promise((resolve, reject) => {

			console.time("VirtualDOMRenderer.render");

			//Dave.ToDo: Qui va inserita tutta la validazione per essere certi che il renderer abbia tutto ciò che gli server.
			Shark.trace("VirtualDOMRenderer.render: ", jaw, options, Shark.TRACE_INFO);

			var defaultOptions = {
				renderOnScreen: true
			};

			options = $.extend(defaultOptions, options);

			if (!jaw.__templateParsed) {
				//Dave.ToDo: Sarebbe interessante poter gestire più template per un singolo componente (tipo, il bottone che apre il modal e il modal) da poter richiamare tipo componente.template
				var templateContent = jaw.templateContent;// || Shark.getTemplate(jaw.templateRoute);

				//Dave.ToDo: Questa cosa andrebbe spostata nell'init, così da non doverlo fare ad ogni render

				// Dave: nei renderer moderni i subTemplate non ci devono essere > andrà sostituito con una gestione dei "fragments"
				// var pc = 0;
				// var subTemplateList = templateContent.match(/{{>[\w\.]+\}\}/g);

				// while (subTemplateList && pc < 100) {
				// 	subTemplateList.forEach(function (item) {
				// 		var componentName = item.replace("{{>", "").replace("}}", "");

				// 		if (componentName) {
				// 			//Dave: ho rimosso "Shark.getTemplate" ma qui non lo tolgo.. se qualcuno lo usa si deve rompere..
				// 			templateContent = templateContent.replace(item, Shark.getTemplate(componentName));
				// 		}
				// 		else {
				// 			Shark.trace("VirtualDOMRenderer.render > Missing SubTemplate: " + componentName, Shark.TRACE_ERROR);
				// 		}
				// 	});

				// 	subTemplateList = templateContent.match(/{{>[\w\.]+\}\}/g);
				// 	pc++;
				// }

				var componentList = templateContent.match(/{{=>[\w\.]+\}\}/g);
				//	var componentTemplates = {};
				while (componentList) {
					componentList.forEach(function (item) {
						let processDefaultTemplate = true;
						let componentName = item.replace("{{=>", "").replace("}}", "");

						if (componentName.indexOf(".") > -1) {
							componentName = componentName.substr(0, componentName.indexOf("."));
							processDefaultTemplate = false;
						}

						let component = Shark.__components[componentName];

						if (component) {
							if (processDefaultTemplate) {
								templateContent = templateContent.replace(item, component.templateContent/* || Shark.getTemplate(component.templateRoute)*/); //templateRoute will be Deprecated
							}

							if (component.__templates) {
								for (let currTemplateName in component.__templates) {
									templateContent = templateContent.replace(`{{=>${componentName}.${currTemplateName}}}`, component.__templates[currTemplateName]);
								}
							}
						}
						else {
							Shark.trace("VirtualDOMRenderer.render > Missing component: " + componentName, Shark.TRACE_ERROR);
							templateContent = templateContent.replace(item, item.replace("{{=>", "{{>"));
						}
					});

					componentList = templateContent.match(/{{=>[\w\.]+\}\}/g);
				}

				//------------------------------------------------------------------------
				let customHtmlComponentRegEx = new RegExp(/<shark-[\w]+.+\/*>/g);
				let customHtmlComponentList = templateContent.match(customHtmlComponentRegEx);

				while (customHtmlComponentList) {
					customHtmlComponentList.forEach(function (item) {
						let componentName = item;
						let templateName = "";

						if (componentName.indexOf("></shark-") > -1) {
							componentName = componentName.substr(0, componentName.indexOf("></shark-"));
						}
						if (componentName.indexOf("/>") > -1) {
							componentName = componentName.substr(0, componentName.indexOf("/>"));
						}
						if (componentName.indexOf(">") > -1) {
							componentName = componentName.substr(0, componentName.indexOf(">"));
						}
						if (componentName.indexOf(" ") > -1) {
							componentName = componentName.substr(0, componentName.indexOf(" "));
						}
						componentName = componentName.replace("<shark-", "");

						if (componentName.indexOf("-") > -1) {
							componentName = Utils.dashedToCamel(componentName);
						}

						let templateAttrStart = item.indexOf("template=\"");
						if (templateAttrStart > -1) {
							templateName = item.substring(templateAttrStart + 10, item.indexOf("\"", templateAttrStart + 11));

							if (templateName === "default") {
								templateName = "";
							}
						}

						let component = Shark.__components[componentName];
						if (component) {
							if (templateName === "") {
								templateContent = templateContent.replace(item, component.templateContent);
							}
							else {
								if (component.__templates && component.__templates[templateName]) {
									templateContent = templateContent.replace(item, component.__templates[templateName]);
								}
							}
						}
						else {
							Shark.trace("VirtualDOMRenderer.render > Missing component: " + componentName + ", removing from template.", Shark.TRACE_ERROR);
							templateContent = templateContent.replace(item, "");
						}
					});

					//Dave.Warn: Se un componente non esiste, si incastra per sempre in questo giro...
					customHtmlComponentList = templateContent.match(customHtmlComponentRegEx);
				}
				//------------------------------------------------------------------------
				templateContent = templateContent.replace(/data-sh-model=\"(.\S+)\"/g, `$& data-shguid="{{$1.__shguid}}"`);
				// templateContent = templateContent.replace(/data-sh-model=\"(.\S+)\"/g, `$& data-__shguid="{{__shguid}}"`);

				jaw.__autoBoundListSection = [];
				const mustacheDynamic = templateContent.match(/{{\[\>[\w\.]+\}\}/g);
				// const mustacheDynamic = /{{\[\>[\w\.]+\}\}/g.exec(templateContent);
				if (mustacheDynamic) {
					mustacheDynamic.forEach(listSection => {
						const propertyName = listSection.replace("{{[>", "").replace("}}", "");
						const mustacheDynamicRE = new RegExp("{{\\[\\>" + propertyName + "\\}\\}([\\s\\S]*){{" + propertyName + "\\<\\]\\}\\}", "g");
						const templates = mustacheDynamicRE.exec(templateContent);
						let itemTemplate = templates[1];

						const tagStart = itemTemplate.indexOf(">");

						itemTemplate = itemTemplate.substr(0, tagStart) + ` data-shguid="{{__shguid}}"` + itemTemplate.substr(tagStart);

						console.log({ listSection });
						jaw.__autoBoundListSection.push({
							propertyName,
							template: itemTemplate
						});

						const openTagRE = new RegExp("{{\\[\\>(" + propertyName + ")\\}\\}", "g");
						const closeTagRE = new RegExp("{{(" + propertyName + ")\\<\\]\\}\\}", "g");

						templateContent = templateContent.replace(openTagRE, "<shark-list property=\"" + propertyName + "\">{{#$1}}");
						templateContent = templateContent.replace(closeTagRE, "{{/$1}}</shark-list>");
					});
					console.log({ __autoBoundListSection: jaw.__autoBoundListSection });
				}

				// /{{\[\>taskList\}\}([\s\S]*){{taskList\<\]\}\}/g
				//Occhio alle {{{ }}}
				//First attempt to manage autorender of bound properties
				console.time("autoBind");
				Shark.trace({
					"renderField": templateContent.match(/{{[\w\.]+\}\}/g),
					"renderListOrPresence": templateContent.match(/{{#[\w\.]+\}\}/g),
					"renderAbsence": templateContent.match(/{{\^[\w\.]+\}\}/g),
				}, Shark.TRACE_LOG);
				Shark.trace({
					"renderField": [...new Set(templateContent.match(/{{[\w\.]+\}\}/g))].map(item => item.replace("{{", "").replace("}}", "")),
					"renderListOrPresence": [...new Set(templateContent.match(/{{#[\w\.]+\}\}/g))].map(item => item.replace("{{#", "").replace("}}", "")),
					"renderAbsence": [...new Set(templateContent.match(/{{\^[\w\.]+\}\}/g))].map(item => item.replace("{{^", "").replace("}}", "")),
				}, Shark.TRACE_LOG);

				const renderField = [...[...new Set(templateContent.match(/{{[\w\.]+\}\}/g))].map(item => item.replace("{{", "").replace("}}", "")),
				...[...new Set(templateContent.match(/{{#[\w\.]+\}\}/g))].map(item => item.replace("{{#", "").replace("}}", "")),
				...[...new Set(templateContent.match(/{{\^[\w\.]+\}\}/g))].map(item => item.replace("{{^", "").replace("}}", ""))];

				Shark.__autoBoundProperties = [];

				for (const value in Shark.__store) {
					if (renderField.indexOf(value) > -1) {
						Shark.__autoBoundProperties.push(value);
						jaw.__autoBoundProperties.push(value);
					}
				}
				Shark.trace({ renderField, SharkAutoBoundProperties: Shark.__autoBoundProperties, JawAutoBoundProperties: jaw.__autoBoundProperties });
				console.timeEnd("autoBind");

				for (const value in Shark.__store) {
					if (Shark.__store[value] instanceof SmartModel) {
						Shark.__store[value].__boundProperties = [];
					}
				}
				// for (const value in Shark.appData) {
				// 	if (Shark.appData[value] instanceof SmartModel) {
				// 		Shark.appData[value].__boundProperties = [];
				// 	}
				// }
				const dynamicFields = templateContent.match(/{{[\w\.]+\}\}/g);
				// var dynamicFields = templateContent.match(/[{[\w\.]+\]\}/g);
				if (dynamicFields && dynamicFields.length > 0) {
					dynamicFields.forEach(item => {
						// const propertyData = item.replace("{[", "").replace("]}", "").split(".");
						const propertyData = item.replace("{{", "").replace("}}", "").split(".");
						const model = Shark.get(propertyData[0]);

						if (model && model instanceof SmartModel) {
							if (model.__boundProperties.indexOf(propertyData[1]) === -1) {
								model.__boundProperties.push(propertyData[1]);
								Shark.trace("dynamicFields", model, "log");
							}
						}

						// templateContent = templateContent.replace(item, "");
					});
				}


				Shark.__dynamicSections = {};
				const dynamicSections = templateContent.match(/\<shark-section(.*?)\>/g);
				// const dynamicSections = templateContent.match(/{{@[\w\.]+\}\}/g);
				if (dynamicSections && dynamicSections.length > 0) {
					dynamicSections.forEach(item => {
						//<shark-section model="commentList">
						//</shark-section>

						const openTag = item; //.replace("@", "#");
						const closeTag = "</shark-section>"; //item.replace("@", "@/");
						const dynamicSection = {
							closeTag,
							openTag,
							rGuid: Shark.__generateGUID("rid_"),
							storeReference: /model=\"(.*?)\"/g.exec(item)[1] // item.replace("{{@", "").replace("}}", "")
						};

						var templateRE = new RegExp(openTag + "(.*?)" + closeTag.replace("/", "\/"), "gs");

						dynamicSection.templateContent = templateRE.exec(templateContent)[1];
						// dynamicSection.templateContent = openTag + templateRE.exec(templateContent)[1] + closeTag;
						if (window.customElements) {
							templateContent = templateContent.replace(item, `<shark-section data-sh-rguid='${dynamicSection.rGuid}'>${openTag}`).replace(closeTag, `${closeTag}</shark-section>`);
						}
						else {
							templateContent = templateContent.replace(item, `<span data-sh-rguid='${dynamicSection.rGuid}'>${openTag}`).replace(closeTag, `${closeTag}</span>`);
						}

						if (Shark.__dynamicSections[dynamicSection.storeReference]) {
							Shark.__dynamicSections[dynamicSection.storeReference].push(dynamicSection);
						}
						else {
							Shark.__dynamicSections[dynamicSection.storeReference] = [dynamicSection];
						}
					});
					Shark.trace("dynamicSections", Shark.__dynamicSections, "log");
				}

				templateContent = templateContent.replace(/\[\[([\w\.]+)\]\]/g, `{{{__renderer.$1}}}`);
				// templateContent = templateContent.replace(/\r\n/g, "");
				templateContent = templateContent.replace(/\t/g, "");
				templateContent = templateContent.replace(/<br[\s\S\ ]*?(?=>)/g, "$&/");
				templateContent = templateContent.replace(/<hr[\s\S\ ]*?(?=>)/g, "$&/");
				templateContent = templateContent.replace(/<img [\s\S\ ]*?(?=>)/g, "$&/");
				templateContent = templateContent.replace(/<input [\s\S\ ]*?(?=>)/g, "$&/");

				jaw.__parsedTemplate = templateContent;
				jaw.__templateParsed = true;
			}

			const componentsStore = {};
			const processedComponentList = [];

			jaw.__activeComponents.forEach(component => {
				if (processedComponentList.indexOf(component.id) === -1) {
					Object.assign(componentsStore, component.__store);
				}

				//Ovviamente questa cosa va estratta per essere recursiva
				component.__activeComponents.forEach(subComponent => {
					if (processedComponentList.indexOf(subComponent.id) === -1) {
						Object.assign(componentsStore, subComponent.__store);
					}
				});
			});

			// jaw.__activeComponents.forEach(function (item) {
			// 	Object.assign(componentsStore, item.__store);
			// });
			const childTemplates = $.extend({}, Shark.__templateMap, jaw.__childTemplates);
			const renderStore = Object.assign({}, Shark.__store, componentsStore, jaw.__store);
			// console.log({ renderStore, Shark__store: Shark.__store, componentsStore, jaw__store: jaw.__store });
			let output = Mustache.render(jaw.__parsedTemplate, renderStore, childTemplates);

			if (options.renderOnScreen) {
				const that = this;

				output = "<span>" + output + "</span>";

				if (!that.tree) {
					//DAve: QUesto è asincrono, dovrei modificarlo per restituire una promise a Shark.render()
					console.time("VirtualDOMRenderer.patch");
					parser(output.trim(), function (err, hscript) {
						that.tree = new Function('h','return '+ hscript)(h);
						that.rootNode = createElement(that.tree);
						$(options.container).append(that.rootNode);
						console.timeEnd("VirtualDOMRenderer.render");
						console.timeEnd("VirtualDOMRenderer.patch");
						resolve();
					});
				}
				else {
					//DAve: QUesto è asincrono, dovrei modificarlo per restituire una promise a Shark.render()
					console.time("VirtualDOMRenderer.patch");
					parser(output.trim(), function (err, hscript) {
						var newTree = new Function('h','return '+ hscript)(h);
						var patches = diff(that.tree, newTree);
						that.rootNode = patch(that.rootNode, patches);
						that.tree = newTree;
						console.timeEnd("VirtualDOMRenderer.render");
						console.timeEnd("VirtualDOMRenderer.patch");
						resolve();
					});
				}
			}
			else {
				resolve(output);
				// return output;
			}
		});
	}
}

export default VirtualDOMRenderer;