Source: libs/sg-model/sg-model-view.js

"use strict";

import SGModel from "./sg-model.js";

/**
 * SGModelView 1.0.3
 * Add-on over SGModel that allows you to bind data in JavaScript with visual elements of HTML document using MVVM pattern.
 * @see https://github.com/VediX/SGModel
 * @copyright 2019-2021 Kalashnikov Ilya
 * @license SGModel may be freely distributed under the MIT license
 * @extends SGModel
 */
class SGModelView extends SGModel {
	
	/**
	 * Set property value. Overriding the **SGModel#set** method
	 * @param {string}	name
	 * @param {mixed}	 val
	 * @param {object}	[options=void 0]
	 * @param {number}		[options.precision] - Rounding precision
	 * @param {mixed}		[options.previous_value] - Use this value as the previous value
	 * @param {number}	[flags=0] - Valid flags: **FLAG_OFF_MAY_BE** | **FLAG_PREV_VALUE_CLONE** | **FLAG_NO_CALLBACKS** | **FLAG_FORCE_CALLBACKS** | **FLAG_IGNORE_OWN_SETTER**
	 * @return {boolean} If the value was changed will return **true**
	 * @override
	 */
	set(name, ...args) {
		if (super.set.apply(this, arguments) && (this._binderInitialized)) {
			this._refreshElement(name);
		}
	}
	
	/**
	 * Perform Data and View Binding (MVVM)
	 * @param {string|HTMLElement} [root=void 0] Example "#my_div_id" or HTMLElement object
	 */
	bindHTML(root = void 0) {
		
		if (! this._binderInitialized) {
			if (typeof document === "undefined") throw "Error! document is undefined!";
			this._onChangeDOMElementValue = this._onChangeDOMElementValue.bind(this);
			this._propertyElementLinks = {};
			this._eventsCounter = -1;
			this._elementsEvents = [];
			this._binderInitialized = true;
		}
		
		if (! root) root = document.body;
		if (typeof root === "string") root = document.querySelector(root);
		for (var name in this._propertyElementLinks) {
			var propertyElementLink = this._propertyElementLinks[name];
			if (propertyElementLink.type === SGModelView._LINKTYPE_VALUE) {
				propertyElementLink.element.removeEventListener("change", this._onChangeDOMElementValue);
				propertyElementLink.element.removeEventListener("input", this._onChangeDOMElementValue);
			}
			delete this._propertyElementLinks[name];
		}
		let item;
		while (item = this._elementsEvents.pop()) {
			item.element.removeEventListener(item.event, item.callback);
		}
		
		this._reProps = {};
		for (var name in this.properties) {
			this._reProps[name] = {
				re: new RegExp("[^\\w\\-]"+name+"[^\\w\\-]|^"+name+"[^\\w\\-]|[^\\w\\-]"+name+"$|^"+name+"$", "g"),
				to: "this.properties." + name
			};
		}
		this._sysThis = ["constructor", "initialize"];
		this._reThis = {};
		let thisNames = Object.getOwnPropertyNames(this.__proto__);
		for (var i = 0; i < thisNames.length; i++) {
			var name = thisNames[i];
			if (this._sysThis.indexOf(name) === -1 && ! /^_/.test(name)) {
				this._reThis[name] = {
					re: new RegExp("[^\\w\\-]"+name+"[^\\w\\-]|^"+name+"[^\\w\\-]|[^\\w\\-]"+name+"$|^"+name+"$", "g"),
					to: "this." + name
				};
			}
		}
		
		this._bindElements([root]);
	}
	
	/** @private */
	_bindElements(elements) {
		for (var e = 0; e < elements.length; e++) {
			var element = elements[e];
			
			if (element.nodeType !== 1) continue;
			
			var sgProperty = element.getAttribute("sg-property");
			var sgType = element.getAttribute("sg-type");
			var sgFormat = element.getAttribute("sg-format");
			//var sgAttributes = element.getAttribute("sg-attributes"); // TODO
			var sgCSS = element.getAttribute("sg-css");
			//var sgStyle = element.getAttribute("sg-style"); // TODO
			var sgClick = element.getAttribute("sg-click");
			//var sgEvents = element.getAttribute("sg-events"); // TODO
			
			var bRefresh = false;
			
			if (sgProperty && this.has(sgProperty)) {
				this._regPropertyElementLink(sgProperty, element, SGModelView._LINKTYPE_VALUE);
				element._sg_property = sgProperty;
				element._sg_type = sgType;
				element._sg_format = this[sgFormat];
				switch (sgType) {
					case "dropdown":
						var eItems = document.querySelectorAll("[sg-dropdown=" + sgProperty + "]");
						for (var i = 0; i < eItems.length; i++) {
							eItems[i].onclick = this._dropdownItemClick;
						}
						element.addEventListener("change", this._onChangeDOMElementValue);
						break;
					default: {
						if (element.type) {
							var sEvent = "";
							switch (element.type) {
								case "range": sEvent = "input"; break;
								case "radio": case "checkbox": case "text": case "button": case "select-one": case "select-multiple": sEvent = "change"; break;
							}
							if (sEvent) {
								element.addEventListener(sEvent, this._onChangeDOMElementValue);
							}
						}
					}
				}
				bRefresh = true;
			}
			
			if (sgCSS) {
				for (var name in this._reProps) {
					var l = sgCSS.length;
					sgCSS = sgCSS.replace(this._reProps[name].re, this._reProps[name].to);
					if (l !== sgCSS.length) {
						this._regPropertyElementLink(name, element, SGModelView._LINKTYPE_CSS);
						bRefresh = true;
					}
				}
				for (var name in this._reThis) {
					sgCSS = sgCSS.replace(this._reThis[name].re, this._reThis[name].to);
				}
				element._sg_css = (new Function("return " + sgCSS)).bind(this);
				element._sg_css_static_classes = [...element.classList];
			}
			
			if (sgClick) {
				let callback = this[sgClick];
				if (typeof callback === "function") {
					callback = callback.bind(this);
					element.addEventListener("click", callback);
					let index = this._eventsCounter++;
					this._elementsEvents[index] = {
						callback: callback,
						element: element,
						event: "click"
					}
				}
			}
			
			if (bRefresh) {
				this._refreshElement(sgProperty);
			}
			
			this._bindElements(element.children);
		}
	}
	
	/** @private */
	_regPropertyElementLink(property, element, type) {
		var item = this._propertyElementLinks[property];
		if (! item) {
			item = this._propertyElementLinks[property] = [];
		}
		if (item.indexOf(element) === -1) {
			item.push({element: element, type: type});
		}
	}
	
	/** @private */
	_refreshElement(property) {
		
		//if (name === "initialized") debugger;
		
		if (! this._propertyElementLinks[property]) return false;
		
		for (var j = 0; j < this._propertyElementLinks[property].length; j++) {
			
			var propertyElementLink = this._propertyElementLinks[property][j];
			
			var element = propertyElementLink.element;
			if (! element) return false;
			
			if (propertyElementLink.type===SGModelView._LINKTYPE_VALUE) {
				
				var value = this.properties[property];

				switch (element._sg_type) {
					case "dropdown":
						var eItems = document.querySelectorAll("[sg-dropdown=" + property + "]");
						for (var i = 0; i < eItems.length; i++) {
							var sgValue = eItems[i].getAttribute("sg-value");
							if (sgValue == value) {
								element.value = value;
								element.innerHTML = eItems[i].innerHTML;
								break;
							}
						}
						break;
					default: {
						if (element.type) {
							switch (element.type) {
								case "radio": case "checkbox": element.checked = value; break;
								case "range": case "text": case "button": case "select-one": element.value = value; break;
								case "select-multiple": {
									if (! Array.isArray(value)) { debugger; break; }
									for (var i = 0; i < element.options.length; i++) {
										let selected = false;
										for (var j = 0; j < value.length; j++) {
											if (element.options[i].value == value[j]) {
												selected = true;
												break;
											}
										}
										element.options[i].selected = selected;
									}
									break;
								}
							}
						} else {
							element.innerHTML = (element._sg_format ? element._sg_format(value) : value);
						}
					}
				}
			}

			if (element._sg_css) {
				for (var i = 0; i < element.classList.length; i++) {
					if (element._sg_css_static_classes.indexOf(element.classList[i]) === -1) {
						element.classList.remove(element.classList[i]);
					}
				}
				let result = element._sg_css();
				if (typeof result === "function") {
					result = result.call(this, property);
				}
				if (! Array.isArray(result)) {
					result = result.split(" ");
				}
				for (var i = 0; i < result.length; i++) {
					element.classList.add(result[i]);
				}
			}
		}
		return true;
	}
	
	/** @private */
	_onChangeDOMElementValue(event) {
		let elem = event.currentTarget;
		switch (elem.type) {
			case "checkbox": this.set(elem._sg_property, elem.checked); break;
			case "radio":
				let form = this._findParentForm(elem);
				let radioButtons = form.querySelectorAll("input[name=" + elem.name+"]");
				for (var i = 0; i < radioButtons.length; i++) {
					var _elem = radioButtons[i];
					if (_elem.getAttribute("sg-property") !== elem.getAttribute("sg-property") && _elem._sg_property) {
						this.set(_elem._sg_property, _elem.checked);
					}
				}
				this.set(elem._sg_property, elem.checked);
				break;
			case "text": case "button": case "select-one": this.set(elem._sg_property, elem.value); break;
			case "range":
				this.set(elem._sg_property, elem.value); break;
			case "select-multiple":
				let result = [];
				for (var i = 0; i < elem.selectedOptions.length; i++) {
					result.push( elem.selectedOptions[i].value );
				}
				this.set(elem._sg_property, result);
				break;
		}
	}
	
	/** @private */
	_dropdownItemClick() {
		let button = this.parentNode.parentNode.querySelector("button");
		button.value = this.getAttribute("sg-value");
		button.innerHTML = this.innerHTML;
		button.dispatchEvent(new Event('change'));
	}
	
	/** @private */
	_findParentForm(elem) {
		let parent = elem.parentNode;
		if (parent) {
			if (parent.tagName === "FORM") {
				return parent;
			} else {
				return this._findParentForm(parent);
			}
		} else {
			return document.body;
		}
	}
}

SGModelView._LINKTYPE_VALUE = 1;
SGModelView._LINKTYPE_CSS = 2;

if (typeof exports === 'object' && typeof module === 'object') module.exports = SGModelView;
else if (typeof define === 'function' && define.amd) define("SGModelView", [], ()=>SGModelView);
else if (typeof exports === 'object') exports["SGModelView"] = SGModelView;
else if (typeof window === 'object' && window.document) window["SGModelView"] = SGModelView;
else this["SGModelView"] = SGModelView;

export default SGModelView;