/**
 * Vvveb
 *
 * Copyright (C) 2021  Ziadin Givan
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * https://github.com/givanz/Vvveb
 */


// Simple JavaScript Templating and buildParams
// John Resig - https://johnresig.com/ - MIT Licensed
(function(){
  let cache = {};
  let startTag = "{%";
  let endTag = "%}";
  let re1 = new RegExp(`((^|${endTag})[^\t]*)'`,"g");
  let re2 = new RegExp(`\t=(.*?)${endTag}`,"g");
  
  this.tmpl = function tmpl(str, data){
    // Figure out if we're getting a template, or if we need to
    // load the template - and be sure to cache the result.
	let fn = /^[-a-zA-Z0-9]+$/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
              
      // Generate a reusable function that will serve as a template
      // generator (and which will be cached).
      new Function("obj",
        "let p=[],print=function(){p.push.apply(p,arguments);};" +
         
        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +
         
        // Convert the template into pure JavaScript
        str
          .replace(/[\r\t\n]/g, " ")
          .split(startTag).join("\t")
          .replace(re1, "$1\r")
          .replace(re2, "',$1,'")
          .split("\t").join("');")
          .split(endTag).join("p.push('")
          .split("\r").join("\\'")
      + "');}return p.join('');");
    // Provide some basic currying to the user
    return data ? fn( data ) : fn;
  };
})();


function buildParams( prefix, obj,  add ) {
	let rbracket = /\[\]$/;

	if ( Array.isArray( obj ) ) {

		// Serialize array item.
		for(const key in obj) {
			let v = obj[key];
			if ( rbracket.test( prefix ) ) {

				// Treat each array item as a scalar.
				add( prefix, v );

			} else {

				// Item is non-scalar (array or object), encode its numeric index.
				buildParams(
					prefix + "[" + ( typeof v === "object" && v != null ? key : "" ) + "]",
					v,
					add
				);
			}
		}

	} else if ( typeof obj === "object" ) {

		// Serialize object item.
		for (const name in obj ) {
			buildParams( prefix + "[" + name + "]", obj[ name ],  add );
		}

	} else {

		// Serialize scalar item.
		add( prefix, obj );
	}
}

// Serialize an array of form elements or a set of
// key/values into a query string
function nestedFormData( a ) {
	let prefix,
		s = [],
		add = function( key, valueOrFunction ) {

			// If value is a function, invoke it and use its return value
			let value = typeof valueOrFunction === "function" ?
				valueOrFunction() :
				valueOrFunction;

			s[ s.length ] = encodeURIComponent( key ) + "=" +
				encodeURIComponent( value == null ? "" : value );
		};

	if ( a == null ) {
		return "";
	}

	// If an array was passed in, assume that it is an array of form elements.
	if ( Array.isArray( a ) || ( Object.is( a ) ) ) {

		// Serialize the form elements
		for(const key in object) {
			let v = object[key];
		//jQuery.each( a, function() {
			add( key, v );
		};

	} else {

		// If traditional, encode the "old" way (the way 1.3.2 or older
		// did it), otherwise encode params recursively.
		for (const prefix in a ) {
			buildParams( prefix, a[ prefix ], add );
		}
	}

	// Return the resulting serialization
	return s.join( "&" );
};


let delay = (function(){
  let timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();

function isElement(obj){
   return (typeof obj==="object") &&
      (obj.nodeType===1) && (typeof obj.style === "object") &&
      (typeof obj.ownerDocument ==="object")/* && obj.tagName != "BODY"*/;
}

function generateElements(html) {
  const template = document.createElement('template');
  template.innerHTML = html.trim();
  return template.content.children;
}

function offset(el) {
  box = el.getBoundingClientRect();
  docElem = document.documentElement;
  return {
    top: box.top + window.pageYOffset - docElem.clientTop,
    left: box.left + window.pageXOffset - docElem.clientLeft
  };
}

if (Vvveb === undefined) var Vvveb = {};

Vvveb.defaultComponent = "_base";
Vvveb.preservePropertySections = true;
//icon = use component icon when dragging | html = use component html to create draggable element
Vvveb.dragIcon = 'icon';
//if empty the html of the component is used to view dropping in real time but for large elements it can jump around for this you can set a html placeholder with this option
Vvveb.dragElementStyle = "background:limegreen;width:100%;height:3px;border:1px solid limegreen;box-shadow:0px 0px 2px 1px rgba(0,0,0,0.14);overflow:hidden;";
Vvveb.dragHtml = '<div style="' + Vvveb.dragElementStyle + '"></div>';

Vvveb.baseUrl =  document.currentScript?document.currentScript.src.replace(/[^\/]*?\.js$/,''):'';
Vvveb.imgBaseUrl =  Vvveb.baseUrl;

Vvveb.ComponentsGroup = {};
Vvveb.SectionsGroup = {};
Vvveb.BlocksGroup = {};
Vvveb.StylesGroup = {};

Vvveb.Components = {
	
	_components: {},
	
	_nodesLookup: {},
	
	_attributesLookup: {},

	_classesLookup: {},
	
	_classesRegexLookup: {},
	
	componentPropertiesElement: "#right-panel .component-properties",

	componentPropertiesDefaultSection: "content",

	get: function(type) {
		return this._components[type];
	},

	updateProperty: function(type, key, value) {
		let properties = this._components[type]["properties"];
		for (property in properties) {
			if (key == properties[property]["key"])  {
				return this._components[type]["properties"][property] = 
				Object.assign(properties[property], value);
			}
		}
	},

	getProperty: function(type, key) {
		let properties = this._components[type] ? this._components[type]["properties"] : [];
		for (property in properties) {
			if (key == properties[property]["key"])  {
				return properties[property];
			}
		}
	},

	add: function(type, data) {
		data.type = type;
		
		this._components[type] = data;
		
		if (data.nodes) {
			for (let i in data.nodes) {	
				this._nodesLookup[ data.nodes[i] ] = data;
			}
		}
		
		if (data.attributes) {
			if (data.attributes.constructor === Array) {
				for (let i in data.attributes) {	
					this._attributesLookup[ data.attributes[i] ] = data;
				}
			} else {
				for (let i in data.attributes) {	
					if (typeof this._attributesLookup[i] === 'undefined') {
						this._attributesLookup[i] = {};
					}

					if (typeof this._attributesLookup[i][ data.attributes[i] ] === 'undefined') {
						this._attributesLookup[i][ data.attributes[i] ] = {};
					}

					this._attributesLookup[i][ data.attributes[i] ] = data;
				}
			}
		}
		
		if (data.classes) {
			for (let i in data.classes) {	
				this._classesLookup[ data.classes[i] ] = data;
			}
		}
		
		if (data.classesRegex) {
			for (let i in data.classesRegex) {	
				this._classesRegexLookup[ data.classesRegex[i] ] = data;
			}
		}
	},
	
	extend: function(inheritType, type, data) {
		 
		 let newData = data;
		 
		 if (inheritData = this._components[inheritType]) {
			newData = {...inheritData, ...data};
			newData.properties = (data.properties ? data.properties : []).concat(inheritData.properties ? inheritData.properties : []);
		 }

		//remove duplicates
		/*
		newData.properties = newData.properties.filter((value, index, self) =>
		  index === self.findIndex((t) => (
			t.key === value.key
		  ))
		);*/

		 //sort by order
		 newData.properties.sort(function (a,b) {
				if (typeof a.sort  === "undefined") a.sort = 0;
				if (typeof b.sort  === "undefined") b.sort = 0;

				if (a.sort < b.sort)
					return -1;
				if (a.sort > b.sort)
					return 1;
				return 0;
			});
		
		this.add(type, newData);
	},
	
	
	matchNode: function(node) {
		let component = {};
		
		if (!node || !node.tagName) return false;
		
		if (node.attributes && node.attributes.length) {
			//search for attributes
			for (let i in node.attributes) {
				if (node.attributes[i]) {
					attr = node.attributes[i].name;
					value = node.attributes[i].value;

					if (attr in this._attributesLookup) {
						component = this._attributesLookup[ attr ];
						
						//currently we check that is not a component by looking at name attribute
						//if we have a collection of objects it means that attribute value must be checked
						if (typeof component["name"] === "undefined") {
							if (value in component) {
								return component[value];
							}
						} else {
							return component;
						}
					}
				}
			}
				
			for (let i in node.attributes) {
				attr = node.attributes[i].name;
				value = node.attributes[i].value;
				
				//check for node classes
				if (attr == "class") {
					classes = value.split(" ");
					
					for (j in classes) {
						if (classes[j] in this._classesLookup)
						return this._classesLookup[ classes[j] ];	
					}
					
					for (regex in this._classesRegexLookup) {
						regexObj = new RegExp(regex);
						if (regexObj.exec(value)) {
							return this._classesRegexLookup[ regex ];	
						}
					}
				}
			}
		}

		tagName = node.tagName.toLowerCase();
		if (tagName in this._nodesLookup) return this._nodesLookup[ tagName ];
	
		return false;
		//return false;
	},
	
	renderProperties: function(component, properties, componentsPanelSections, container) {
		let fn = function(component, property) {
			if (property.input) {
				property.input.addEventListener('propertyChange', (event) => {
						element = selectedElement = Vvveb.Builder.selectedEl;
						let value = event.detail.value, input = event.detail.input, origEvent = event.detail.origEvent;
						
						if (property.element) element = property.element;
						if (property.child) element = element.querySelector(property.child);
						if (property.parent) element = element.parent(property.parent);

						if (property.onChange) {
							let ret = property.onChange(element, value, input, component, origEvent);
							//if on change returns an object then is returning the dom node otherwise is returning the new value
							if (typeof ret == "object")  {
								element = ret;
							} else {
								value = ret;
							}
						}/* else */
						if (property.htmlAttr) {
							let oldValue;
							
							if (property.htmlAttr == "class" && property.validValues) {
								oldValue = element.getAttribute(property.htmlAttr);
								
								if (property.validValues) {
									element.classList.remove(...property.validValues.filter(v => v));
								}
								if (value) {
									element.classList.add(...value.split(" "));
								}
							}
							else if (property.htmlAttr == "style") {
								//keep old style for undo
								oldStyle = window.FrameDocument.getElementById("vvvebjs-styles").textContent;
								element = Vvveb.StyleManager.setStyle(element, property.key, value);
							} else if (property.htmlAttr == "innerHTML")  {
								oldValue = element.innerHTML;
								element = Vvveb.ContentManager.setHtml(element, value);
							} else if (property.htmlAttr == "outerHTML")  {
								oldValue = element.outerHTML;
								element = Vvveb.ContentManager.setHtml(element, value, true);
							} else if (property.htmlAttr == "innerText")  {
								oldValue = element.innerText;
								element = Vvveb.ContentManager.setText(element, value);
							} else {
								oldValue = element.getAttribute(property.htmlAttr);
								//if value is empty then remove attribute useful for attributes without values like disabled
								if (value) {
									element.setAttribute(property.htmlAttr, value);
								} else {
									element.removeAttribute(property.htmlAttr);
								}
							}
							
							if (property.htmlAttr == "style") {
								mutation = {
									type: 'style', 
									target: element, 
									attributeName: property.htmlAttr, 
									oldValue: oldStyle, 
									newValue: window.FrameDocument.getElementById("vvvebjs-styles").textContent};
								
								Vvveb.Undo.addMutation(mutation);
							} else {
								Vvveb.Undo.addMutation({
									type: 'attributes', 
									target: element, 
									attributeName: property.htmlAttr, 
									oldValue: oldValue, 
									newValue: value
								});
							}
						}

						if (component && component.onChange) {
							element = component.onChange(element, property, value, input);
						}
						
						if (property.child || property.parent) {
							Vvveb.Builder.selectNode(selectedElement);
						} else {
							Vvveb.Builder.selectNode(element);
						}
						
						return element;
				});		
			}
			
			return property.input;
		};
		
		let element;
		let defaultSection = this.componentPropertiesDefaultSection;
		let section;
		if (componentsPanelSections) {
			section = componentsPanelSections[defaultSection].querySelector('.section[data-section="default"]');
		}
		let nodeElement = Vvveb.Builder.selectedEl;
		
		for (let i in properties) {
			let property = properties[i];
			let element = nodeElement;

			if (property.beforeInit) property.beforeInit(element);
			
			if (property.element) element = property.element;
			if (property.child) element = element.querySelector(property.child) ?? element;
			if (property.parent) element = element.closest(property.parent) ?? element;

			if (property.data) {
				property.data["key"] = property.key;
			} else {
				property.data = {"key" : property.key};
			}

			if (typeof property.group  === 'undefined') property.group = null;

			property.input = property.inputtype.init(property.data, element);
			
			let value;
			if (property.init) {
				property.inputtype.setValue(property.init(element));
			} else if (property.htmlAttr) {
				if (property.htmlAttr == "style") {
					//value = element.css(property.key);//jquery css returns computed style
					value = Vvveb.StyleManager.getStyle(element, property.key);//getStyle returns declared style
				} else
				if (property.htmlAttr == "innerHTML") {
					value = Vvveb.ContentManager.getHtml(element);
				} else if (property.htmlAttr == "outerHTML") {
					value = Vvveb.ContentManager.getHtml(element, true);
				} else if (property.htmlAttr == "innerText") {
					value = Vvveb.ContentManager.getText(element);
				} else {
					value = element.getAttribute(property.htmlAttr);
				}

				//if attribute is class check if one of valid values is included as class to set the select
				if (value && property.htmlAttr == "class" && property.validValues) {
					let valid = value.split(" ").filter(function(el) {
						return property.validValues.indexOf(el) != -1
					});
					
					if (valid && valid.length) {
						value = valid[0];
					} else  {
						value = "";
					}
				} 
				
				if (!value && property.defaultValue) {
					value = property.defaultValue;
				}

				property.inputtype.setValue(value);
			} else {
				if (!value && property.defaultValue) {
					value = property.defaultValue;
				}

				property.inputtype.setValue(value);
			}
			
			fn(component, property);
			
			let propertySection = defaultSection;
			if (property.section) {
				propertySection = property.section;
			}			

			if (property.inputtype == SectionInput) {
				section = componentsPanelSections[propertySection].querySelector('.section[data-section="' + property.key + '"]');
				
				if (Vvveb.preservePropertySections && section) {
					section.replaceChildren();
				} else  {
					componentsPanelSections[propertySection].append(property.input);
					section = componentsPanelSections[propertySection].querySelector('.section[data-section="' + property.key + '"]');
				}
			}
			else {
				let row = generateElements(tmpl('vvveb-property', property))[0]; 
				row.querySelector('.input').append(property.input);
				if (container) {
					container.append(row);
				} else {
					section.append(row);
				}
			}
			
			if (property.inputtype.afterInit) {
				property.inputtype.afterInit(property.input);
			}

			if (property.afterInit) {
				property.afterInit(element, property.input);
			}
		}
	},
	
	render: function(type, panel = false) {

		let component = this._components[type];
		if (!component) return;

		if (!panel) {
			//panel = document.querySelector(this.componentPropertiesElement);
			panel = this.componentPropertiesElement;
		}

		let defaultSection = this.componentPropertiesDefaultSection;
		let componentsPanelSections = {};

		document.querySelectorAll(panel + " .tab-pane").forEach((el, i) => {
			let sectionName = el.dataset.section;
			componentsPanelSections[sectionName] = el;
			for (const item of el.querySelectorAll(
				'label:not([data-header="default"]) + input,' +
				'label:not([data-header="default"]),' +
				'.section:not([data-section="default"])'
			)) {
			  item.remove();
			}
		});
		
		let section = componentsPanelSections[defaultSection].querySelector('.section[data-section="default"]');
		
		if (!(Vvveb.preservePropertySections && section)) {
			let template = tmpl("vvveb-input-sectioninput", {key:"default", header:component.name});

			componentsPanelSections[defaultSection].replaceChildren();
			componentsPanelSections[defaultSection].append(generateElements(template)[0]);
			
			section = componentsPanelSections[defaultSection].querySelector(".section");
		}

		componentsPanelSections[defaultSection].querySelector('[data-header="default"] span').innerHTML = component.name;
		section.replaceChildren();	
	
		if (component.beforeInit) component.beforeInit(Vvveb.Builder.selectedEl);

		this.renderProperties(component, component.properties, componentsPanelSections);
		
		if (component.init) component.init(Vvveb.Builder.selectedEl);
	}
};	


Vvveb.Blocks = {
	
	_blocks: {},

	get: function(type) {
		return this._blocks[type];
	},

	add: function(type, data) {
		data.type = type;
		this._blocks[type] = data;
	},
};	

Vvveb.Sections = {
	
	_sections: {},

	get: function(type) {
		return this._sections[type];
	},

	add: function(type, data) {
		data.type = type;
		this._sections[type] = data;
	},
};	

Vvveb.Styles = {
	
	_styles: {},

	get: function(type) {
		return this._styles[type];
	},
	
	getByStyle: function(style) {
		for (const name in this._styles) {
			if (this._styles[name].style == style) {
				return name;
			}
		}
	},

	add: function(type, data) {
		data.type = type;
		this._styles[type] = data;
	},
};	


Vvveb.WysiwygEditor = {
	
	isActive: false,
	oldValue: '',
	doc:false,
	

	editorSetStyle: function (tag, style = {}, toggle = false) {
		let iframeWindow = Vvveb.Builder.iframe.contentWindow;
		let selection = iframeWindow.getSelection();
		let element = this.element;
		let range;

		if (!tag) {
			tag = "span";
		}

		if (selection.rangeCount > 0) {
			//check if the whole text is inside an existing node to use the node directly
			if ((selection.baseNode && selection.baseNode.nextSibling == null && selection.baseNode.previousSibling == null 
				&& selection.anchorOffset == 0 && selection.focusOffset == selection.baseNode.length) 
				|| (selection.anchorOffset == selection.focusOffset)) {
					
				element = selection.baseNode.parentNode;
				
			} else {
				element = document.createElement(tag);
				range = selection.getRangeAt(0);
				
				try {
					range.surroundContents(element);
					range.selectNodeContents(element.childNodes[0], 0); 

				} catch (e) {
					let content = range.extractContents();

					element.appendChild(content);
					range.insertNode(element);

					range.selectNodeContents(element);
				}				
			}
		}
		
		if (element && style) {
			for (name in style) {

				if ( !style[name] || 
					(toggle && element.style.getPropertyValue(name))) {

					element.style.removeProperty(name);
					
				} else {
					element.style.setProperty(name, style[name]);
				}
			}
		}
		
		//if edited text is an empty span remove the span
		if (element.tagName == "SPAN" && element.style.length == 0 && element.attributes.length <= 1) {
			let textNode = iframeWindow.document.createTextNode(element.innerText);
			element.replaceWith(textNode);
			element = textNode;

			range = iframeWindow.document.createRange();
			range.selectNodeContents(element);
			selection.removeAllRanges();
			selection.addRange(range);		
        }
		
		//select link element to edit link etc
		if (tag == "a") {
			Vvveb.Builder.selectNode(element);
			Vvveb.Builder.loadNodeComponent(element);
		}
		return element;
	},
	
	init: function(doc) {
		this.doc = doc;
		let self = this;
		
		document.getElementById("bold-btn").addEventListener("click", function (e) {
				//doc.execCommand('bold',false,null);
				//self.editorSetStyle("b", {"font-weight" : "bold"}, true);
				self.editorSetStyle(false, {"font-weight" : "bold"}, true);
				e.preventDefault();
				return false;
		});

		document.getElementById("italic-btn").addEventListener("click", function (e) {
				//doc.execCommand('italic',false,null);
				//self.editorSetStyle("i", {"font-style" : "italic"}, true);
				self.editorSetStyle(false, {"font-style" : "italic"}, true);
				e.preventDefault();
				return false;
		});

		document.getElementById("underline-btn").addEventListener("click", function (e) {
				//doc.execCommand('underline',false,null);
				//self.editorSetStyle("u", {"text-decoration" : "underline"}, true);
				self.editorSetStyle(false, {"text-decoration" : "underline"}, true);
				e.preventDefault();
				return false;
		});
		
		document.getElementById("strike-btn").addEventListener("click", function (e) {
				//doc.execCommand('strikeThrough',false,null);
				//self.editorSetStyle("strike",  {"text-decoration" : "line-through"}, true);
				self.editorSetStyle(false,  {"text-decoration" : "line-through"}, true);
				e.preventDefault();
				return false;
		});

		document.getElementById("link-btn").addEventListener("click", function (e) {
				//doc.execCommand('createLink',false,"#");
				self.editorSetStyle("a");
				e.preventDefault();
				return false;
		});

		document.getElementById("fore-color").addEventListener("change", function (e) {
				//doc.execCommand('foreColor',false,this.value);
				self.editorSetStyle(false, {"color" : this.value});
				e.preventDefault();
				return false;
		});

		
		document.getElementById("back-color").addEventListener("change", function (e) {
				//doc.execCommand('hiliteColor',false,this.value);
				self.editorSetStyle(false, {"background-color" : this.value});
				e.preventDefault();
				return false;
		});
		
		document.getElementById("font-size").addEventListener("change", function (e) {
				//doc.execCommand('fontSize',false,this.value);
				self.editorSetStyle(false, {"font-size" : this.value});
				e.preventDefault();
				return false;
		});

		let sizes = "<option value=''> - Font size - </option>";
		for (i = 1;i <= 128; i++) {
			sizes += "<option value='"+ i +"px'>"+ i +"</option>";
		}
		document.getElementById("font-size").innerHTML = sizes;		

		document.getElementById("font-family").addEventListener("change", function (e) {
				let option = this.options[this.selectedIndex];
				let element = self.editorSetStyle(false, {"font-family" : this.value});
				Vvveb.FontsManager.addFont(option.dataset.provider, this.value, element);
				//doc.execCommand('fontName',false,this.value);
				e.preventDefault();
				return false;
		});

		document.getElementById("justify-btn").addEventListener("click", function (e) {
				//let command = "justify" + this.dataset.value;
				//doc.execCommand(command,false,"#");
				
				self.editorSetStyle(false, {"text-align" : e.srcElement.dataset.value});
				e.preventDefault();
				return false;
		});

		doc.addEventListener('keydown', event => {
		  if (event.key === 'Enter') {
			  let target = event.target.closest("[contenteditable]");
			  if (target) {
				doc.execCommand('insertLineBreak');
				event.preventDefault();
			  }
		  }
		})		
	},
	
	undo: function(element) {
		this.doc.execCommand('undo',false,null);
	},

	redo: function(element) {
		this.doc.execCommand('redo',false,null);
	},
	
	edit: function(element) {
		element.setAttribute("contenteditable", true);
		element.setAttribute("spellcheckker", false);
		document.getElementById("wysiwyg-editor").style.display = "block";

		this.element = element;
		this.isActive = true;
		this.oldValue = element.innerHTML;

		document.getElementById("font-family").value = Vvveb.StyleManager.getStyle(element,'font-family');
		document.getElementById("fore-color").value = Vvveb.StyleManager.getStyle(element,'color');
		document.getElementById("back-color").value = Vvveb.StyleManager.getStyle(element,'background-color');
		element.focus();
	},

	destroy: function(element) {
		element.removeAttribute("contenteditable");
		element.removeAttribute("spellcheckker");
 
		document.getElementById("wysiwyg-editor").style.display = "none";
		this.isActive = false;

	
		node = this.element;
		Vvveb.Undo.addMutation({type:'characterData', 
								target: node, 
								oldValue: this.oldValue, 
								newValue: node.innerHTML});
	}
}
	
Vvveb.Builder = {

	component : {},
	dragMoveMutation : false,
	isPreview : false,
	runJsOnSetHtml : false,
	designerMode : false,
	highlightEnabled : false,
	selectPadding: 0,
	leftPanelWidth: 275,
	ignoreClasses: ["clearfix", "masonry", "has-shadow"],
	
	init: function(url, callback) {

		let self = this;
		
		self.loadControlGroups();
		self.loadBlockGroups();
		self.loadSectionGroups();
		self.loadStylesGroups();
		
		self.selectedEl = null;
		self.highlightEl = null;
		self.initCallback = callback;
		
        self.documentFrame = document.querySelector("#iframe-wrapper > iframe");
        self.canvas = document.getElementById("canvas");

		self._loadIframe(url + (url.indexOf('?') > -1 ? '&r=':'?r=') + Math.random());
		
		self._initDragdrop();
		
		self._initBox();

		self.dragElement = null;
		
		self.highlightEnabled = true;
		
		self.leftPanelWidth = document.getElementById("left-panel").clientWidth;
	},
	
/* controls */    	
	loadControlGroups : function() {	

		let componentsList = document.querySelectorAll(".components-list");
		let item = {}, component = {};
		let count = 0;
		
		componentsList.forEach(function (list, i) {
			let type = list.dataset.type;
			list.replaceChildren();
			count ++;

			for (group in Vvveb.ComponentsGroup) {
				
				list.append(generateElements(
				`<li class="header" data-section="${group}"  data-search="">
					<label class="header" for="${type}_comphead_${group}${count}">
						${group}<div class="header-arrow"></div>
					</label>
					<input class="header_check" type="checkbox" checked="true" id="${type}_comphead_${group}${count}">
					<ol></ol>
				</li>`)[0]);
								
				//list.append('<li class="header clearfix" data-section="' + group + '"  data-search=""><label class="header" for="' + type + '_comphead_' + group + count + '">' + group + '  <div class="header-arrow"></div>\
					//				   </label><input class="header_check" type="checkbox" checked="true" id="' + type + '_comphead_' + group + count + '">  <ol></ol></li>');

				let componentsSubList = list.querySelector('li[data-section="' + group + '"]  ol');
				
				components = Vvveb.ComponentsGroup[ group ];
				
				for (i in components) {
					const componentType = components[i];
					component = Vvveb.Components.get(componentType);
					
					if (component) {
						item = generateElements(`<li data-section="${group}" data-drag-type="component" data-type="${componentType}" data-search="${component.name.toLowerCase()}">
							<span>${component.name}</span>
						</li>`)[0];

						if (component.image) {

							item.style.backgroundImage = "url(" + Vvveb.imgBaseUrl + component.image + ")"; 			
							item.style.backgroundRepeat = "no-repeat";
						}
						
						componentsSubList.append(item);
					}
				}
			}
		});
	 },
	 
	loadSectionGroups : function() {	

		let sectionsList = document.querySelectorAll(".sections-list");
		let item = {};

		sectionsList.forEach(function (list, i) {
			let type = list.dataset.type;
			list.replaceChildren();

			for (group in Vvveb.SectionsGroup) {
				list.append(generateElements(
				`<li class="header" data-section="${group}"  data-search="">
					<label class="header" for="${type}_sectionhead_${group}">
						${group}<div class="header-arrow"></div>
					</label>
					<input class="header_check" type="checkbox" checked="true" id="${type}_sectionhead_${group}">
					<ol></ol>
				</li>`)[0]);

				let sectionsSubList = list.querySelector('li[data-section="' + group + '"]  ol');
				let sections = Vvveb.SectionsGroup[ group ];

				for (i in sections) {
					const sectionType = sections[i];
					const section = Vvveb.Sections.get(sectionType);
					
					if (section) {
						item = generateElements(`<li data-section="${group}" data-drag-type="section" data-type="${sectionType}" data-search="${section.name.toLowerCase()}">
									<span class="name">${section.name}</span>
									<div class="add-section-btn" title="Add section"><i class="la la-plus"></i></div>
									<img class="preview" src="" loading="lazy">
								</li>`)[0];

						if (section.image) {

							let image = ((section.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + section.image;
							item.querySelector("img").setAttribute("src", image);
						}
						
						sectionsSubList.append(item)
					}
				}
			}
		});
	 },
	 
	loadBlockGroups : function() {	

		let blocksList = document.querySelectorAll(".blocks-list");
		let item = {};

		blocksList.forEach(function (list, i) {
			let type = list.dataset.type;
			list.replaceChildren();

			for (group in Vvveb.BlocksGroup) {
				list.append(generateElements(
				`<li class="header" data-section="${group}"  data-search="">
					<label class="header" for="${type}_blockhead_${group}">
						${group}<div class="header-arrow"></div>
					</label>
					<input class="header_check" type="checkbox" checked="true" id="${type}_blockhead_${group}">
					<ol></ol>
				</li>`)[0]);

				let blocksSubList = list.querySelector('li[data-section="' + group + '"]  ol');
				blocks = Vvveb.BlocksGroup[ group ];

				for (i in blocks) {
					const blockType = blocks[i];
					const block = Vvveb.Blocks.get(blockType);
					
					if (block) {
						item = generateElements(`<li data-section="${group}" data-drag-type="block" data-type="${blockType}" data-search="${block.name.toLowerCase()}">
									<span class="name">${block.name}</span>
									<img class="preview" src="" loading="lazy">
								</li>`)[0];

						if (block.image) {

							let image = ((block.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + block.image;
							item.querySelector("img").setAttribute("src", image);
						}
						
						blocksSubList.append(item);
					}
				}
			}
		});
	 },	
	 
	 loadStylesGroups : function() {	

		let stylesList = document.querySelectorAll(".styles-list");
		let item = {};

		stylesList.forEach(function (list, i) {
			let type = list.dataset.type;
			list.replaceChildren();

			for (group in Vvveb.StylesGroup) {
				list.append(generateElements(
				`<li class="header" data-section="${group}"  data-search="">
					<label class="header" for="${type}_stylehead_${group}">
						${group}<div class="header-arrow"></div>
					</label>
					<input class="header_check" type="checkbox" checked="true" id="${type}_stylehead_${group}">
					<ol></ol>
				</li>`)[0]);

				let stylesSubList = list.querySelector('li[data-section="' + group + '"]  ol');
				styles = Vvveb.StylesGroup[ group ];

				for (i in styles) {
					const styleType = styles[i];
					const style = Vvveb.Styles.get(styleType);
					
					if (style) {
						item = generateElements(`<li data-section="${group}" data-type="${styleType}" data-search="${style.name.toLowerCase()}">
									<span class="name">${style.name}</span>
									<img class="preview" src="" loading="lazy">
								</li>`)[0];

						if (style.image) {

							let image = ((style.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + style.image;
							item.querySelector("img").setAttribute("src", image);
						}
						
						stylesSubList.append(item);
					}
				}
			}
		});
	 },
	
	loadUrl : function(url, callback) {	
		let self = this;
		document.getElementById("select-box").style.display = "none";
		
		self.initCallback = callback;
		if (Vvveb.Builder.iframe.src != url) Vvveb.Builder.iframe.src = url;
	},
	
/* iframe */
	_loadIframe : function(url) {	

		let self = this;
		self.iframe = this.documentFrame;
		self.iframe.src = url;

	    return this.documentFrame.addEventListener("load", function() {
				window.FrameWindow = self.iframe.contentWindow;
				window.FrameDocument = self.iframe.contentWindow.document;
				let addSectionBox = document.getElementById("add-section-box"); 
				let highlightBox = document.getElementById("highlight-box");
				let SelectBox = document.getElementById("select-box");
				
				highlightBox.style.display = "none"; 
				

				window.FrameWindow.addEventListener("beforeunload", function(event) {
					if (Vvveb.Undo.undoIndex >= 0) {
						let dialogText = "You have unsaved changes";
						event.returnValue = dialogText;
						return dialogText;
					}
				});
				
				window.FrameWindow.addEventListener("unload", function(event) {
					document.querySelector(".loading-message").classList.add("active");
					Vvveb.Undo.reset();
				});
				
				//prevent accidental clicks on links when editing text
				window.FrameDocument.addEventListener("click", function(event) {
					if (Vvveb.WysiwygEditor.isActive && event.target.closest("a"))  {
						event.preventDefault();
						return false;
					}
				});
				
				selectBoxPosition = function(event) {
						let pos;
						let target = self.selectedEl;// ?? self.highlightEl;
						
						highlightBox.style.display = "none"; 

						if (target) {
							pos = offset(target);
						
							SelectBox.style.top  = (pos.top - (self.frameDoc.scrollTop ?? 0)  - self.selectPadding) + "px"; 
							SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px";

							SelectBox.style.width = ((target.offsetWidth ?? target.clientWidth) + self.selectPadding * 2) + "px"; 			
							SelectBox.style.height = ((target.offsetHeight ?? target.clientHeight) + self.selectPadding * 2) + "px";
						}
				}
				
				window.FrameWindow.addEventListener("scroll", selectBoxPosition);
				window.FrameWindow.addEventListener("resize", selectBoxPosition);
			
				Vvveb.WysiwygEditor.init(window.FrameDocument);
				Vvveb.StyleManager.init(window.FrameDocument);
				Vvveb.ColorPaletteManager.init(window.FrameDocument);

				if (self.initCallback) self.initCallback();

                return self._frameLoaded();
        });		
        
	},	
    
    _frameLoaded : function() {
		
		let self = Vvveb.Builder;
		
		self.frameDoc  = window.FrameDocument;
		self.frameHtml = window.FrameDocument.querySelector("html");
		self.frameBody = window.FrameDocument.querySelector("body");
		self.frameHead = window.FrameDocument.querySelector("head");
		
		//insert editor helpers like non editable areas
		self.frameHead.append(generateElements('<link data-vvveb-helpers href="' + Vvveb.baseUrl + '../../css/vvvebjs-editor-helpers.css" rel="stylesheet">')[0]);
		self.frameHtml.setAttribute("data-vvvebjs-editor","");

		self._initHighlight();
		
		window.dispatchEvent(new CustomEvent("vvveb.iframe.loaded", {detail: self.frameDoc}));

		document.querySelector(".loading-message").classList.remove("active");
		Vvveb.NewSection.init();
		Vvveb.StyleList.load(self.frameDoc);
		
		//enable save button only if changes are made
		let setSaveButtonState = function (e) { 
			if (Vvveb.Undo.hasChanges()){
				document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled"));
			} else {
				document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
			}
		};		
		
		Vvveb.Builder.frameBody.addEventListener("vvveb.undo.add", setSaveButtonState);		
		Vvveb.Builder.frameBody.addEventListener("vvveb.undo.restore", setSaveButtonState);		
    },	
    
    _getElementType: function(el) {
		
		//search for component attribute
		let componentName = '';  
		let componentAttribute = '';  
		   
		if (el.attributes) {
			for (let j = 0; j < el.attributes.length; j++){
			  let nodeName = el.attributes[j].nodeName;	
			  
			  if (nodeName.indexOf('data-component') > -1)	 {
				componentName = nodeName.replace('data-component-', '');	
				return [componentName, "component"];
			  }		  
			  
			  if (nodeName.indexOf('data-v-component-') > -1)	 {
				componentName = nodeName.replace('data-v-component-', '');	
				return [componentName,"component"];
			  }

			  if (nodeName.indexOf('data-v-') > -1)	 {
				componentAttribute = (componentAttribute ? componentAttribute + " - " : "") + 
										nodeName.replace('data-v-', '') + " ";	
			  }
			}
		}

		if (componentAttribute != '') return [componentAttribute, "attribute"];
		
		if (el.id) {
			componentName = "#" + el.id;
		} else {
			componentName = (el.className && (typeof el.className == "string")) ? "." + el.className.split(" ")[0] : "";
		}
		
		return [componentName, el.tagName];
	},
	
	loadNodeComponent:  function(node) {
		const data = Vvveb.Components.matchNode(node);
		let component;
		
		if (data) 
			component = data.type;
		else 
			component = Vvveb.defaultComponent;
			
		Vvveb.component = Vvveb.Components.get(component);	
		Vvveb.Components.render(component);
		this.selectedComponent = component;

		//if component properties is loaded in left panel tab instead of right panel show tab
		let propertiesTab = document.querySelector(".component-properties-tab a");
		if (propertiesTab.offsetParent) {//if properites tab is enabled/visible 
				propertiesTab.style.display = "";
				const bsTab = bootstrap.Tab.getOrCreateInstance(propertiesTab);
				bsTab.show(); 
		}		
	},
	
	reloadComponent:  function() {
		Vvveb.Components.render(this.selectedComponent);
	},
	
	moveNodeUp:  function(node) {
		if (!node) {
			node = Vvveb.Builder.selectedEl;
		}

		const oldParent = node.parentNode;
		const oldNextSibling = node.nextSibling;
		const next = node.previousElementSibling;
		
		if (next) {
			next.before(node);
		} else {
			node.parentNode.before(node);
		}
		
		Vvveb.Builder.selectNode(node);

		const newParent = node.parentNode;
		const newNextSibling = node.nextSibling;
		
		Vvveb.Undo.addMutation({type: 'move', 
								target: node,
								oldParent: oldParent,
								newParent: newParent,
								oldNextSibling: oldNextSibling,
								newNextSibling: newNextSibling});

	},

	moveNodeDown:  function(node) {
			if (!node) {
				node = Vvveb.Builder.selectedEl;
			}

			const oldParent = node.parentNode;
			const oldNextSibling = node.nextSibling;
			const next = node.nextElementSibling;
			
			if (next) {
				next.after(node);
			} else {
				node.parentNode.after(node);
			}
			
			Vvveb.Builder.selectNode(node);
			
			const newParent = node.parentNode;
			const newNextSibling = node.nextSibling;
			
			Vvveb.Undo.addMutation({type: 'move', 
									target: node,
									oldParent: oldParent,
									newParent: newParent,
									oldNextSibling: oldNextSibling,
									newNextSibling: newNextSibling});
	},

	cloneNode:  function(node) {
		if (!node) {
			node = Vvveb.Builder.selectedEl;
		}

		const clone = node.cloneNode(true);
		node.after(clone);
		node.click();
		
		Vvveb.Undo.addMutation({type: 'childList', 
								target: node.parentNode, 
								addedNodes: [clone],
								nextSibling: node.nextSibling});
		
	},
	
	
	selectNode:  function(node) {
		let SelectBox = document.getElementById("select-box");
		
		if (!node) {
			SelectBox.style.display = "none";
			return;
		}

		let self = this;
		let SelectActions = document.getElementById("select-actions");
		let AddSectionBtn = document.getElementById("add-section-btn");
		let elementType = this._getElementType(node);
		
		if (self.texteditEl && (self.selectedEl != node)) {
			Vvveb.WysiwygEditor.destroy(self.texteditEl);
			self.selectPadding = 0;
			SelectBox.classList.remove("text-edit");
			SelectActions.style.display = "";
			self.texteditEl = null;
		}

		if (elementType[1] == "BODY") {
			SelectActions.style.display = "none";
			AddSectionBtn.style.display = "none";
		} else {
			SelectActions.style.display = "";
			AddSectionBtn.style.display = "";
		}

		let target = node;
		self.selectedEl = target;

		try {
			let pos = offset(target);
			let top = (pos.top - (self.frameDoc.scrollTop ?? 0)  - self.selectPadding);
				
			SelectBox.style.top  = top + "px"; 
			SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px"; 			
			SelectBox.style.width = ((target.offsetWidth ?? target.clientWidth) + self.selectPadding * 2) + "px"; 			
			SelectBox.style.height = ((target.offsetHeight ?? target.clientHeight) + self.selectPadding * 2) + "px";
			SelectBox.style.display = "block";
			
			//move actions toolbar to bottom if there is no space on top
			if (top < 30) {
				SelectActions.style.top = "unset";
				SelectActions.style.bottom = "-25px";
			} else {
				SelectActions.style.top = "";
				SelectActions.style.bottom = "";
			}
				 
			Vvveb.Breadcrumb.loadBreadcrumb(target);
		
		} catch(err) {
			console.log(err);
			return false;
		}
			 
		document.querySelector("#highlight-name .type").innerHTML = elementType[0];
		document.querySelector("#highlight-name .name").innerHTML = elementType[1];
		
		window.dispatchEvent(new CustomEvent("vvveb.Builder.selectNode", {detail: {target}}));
	},

/* iframe highlight */    
    _initHighlight: function() {
		
		let self = Vvveb.Builder;
		
		let highlightMove = function(event) {
			if (self.highlightEnabled == true && event.target/* && isElement(event.target)*/) {

				self.highlightEl = target = event.target;
				let pos = offset(target);
				let height = target.offsetHeight;
				let halfHeight = Math.max(height / 2, 5);
				let width = target.offsetWidth;
				let halfWidth = Math.max(width / 2, 5);
				let prepend = true;
				
				let x = event.x;
				let y = event.y;

				if (self.isResize) {
					if (!self.initialPosition) {
						self.initialPosition = {x,y};
					}
					
					let deltaX = x - self.initialPosition.x; 
					let deltaY = y - self.initialPosition.y; 
					
					pos = offset(self.selectedEl);
					
					width = self.initialSize.width;
					height = self.initialSize.height;
					
					switch (self.resizeHandler) {
						// top
						case "top-left":
							height -= deltaY; 
							width -= deltaX; 
						break;
						
						case "top-center":
							height -= deltaY; 
						break;
						
						case "top-right":
							height -= deltaY; 
							width += deltaX; 
						break;
						
						// center 
						case "center-left":
							width -= deltaX; 
						break;
						
						case "center-right":
							width += deltaX; 
						break;
						
						// bottom 
						case "bottom-left":
							width -= deltaX; 
							height += deltaY; 
						break;
						
						case "bottom-center":
							height += deltaY; 
						break;
						
						case "bottom-right":
							width += deltaX; 
							height += deltaY; 
						break;
					}
				
				    if (self.resizeMode == "css") {
				        self.selectedEl.style.width = width + "px";
				        self.selectedEl.style.height = height + "px";
				    } else {
				        self.selectedEl.setAttribute("width", width);
				        self.selectedEl.setAttribute("height", height);
				    }

					let SelectBox = document.getElementById("select-box");
					SelectBox.style.top  = pos.top - (self.frameDoc.scrollTop ?? 0) + "px"; 
					SelectBox.style.left = pos.left - (self.frameDoc.scrollLeft ?? 0) + "px"; 			
					SelectBox.style.width = width + "px"; 			
					SelectBox.style.height = self.selectedEl.offsetHeight + "px";
					SelectBox.style.display = "block";
				
				} else if (self.isDragging) {
					let noChildren = {
						input: true,
						textarea: true,
						img: true,
						svg: true,
						iframe: true,
						embed: true,
						col: true,
						area: true,
						hr: true,
						br: true,
						wbr: true
					};

					let parent = self.highlightEl;

					let isBlock = (window.getComputedStyle(parent).display == "block");

					if (self.dragType == "section") {
						let closest = parent.closest("section, header, footer, body");
						if (closest) {
							parent = closest;
						}
						noChildren.section = true;
					}

					let parentTagName = parent.tagName.toLowerCase();
					let isVattribute = false;
					//check if node is a data-v-attribute dynamic node that will override the content if added inside
					if (parent.childElementCount == 0) {
						for (let attr of parent.attributes) {
							if (attr.name.startsWith("data-v-") && !attr.name.startsWith("data-v-component-")) {
								isVattribute = true;
								break;
							}
						}
					}
					
					try {
							if ((pos.top  < (y - halfHeight)) || (pos.left  < (x - halfWidth))) {
								if (noChildren[parentTagName] || isBlock || isVattribute) { 
									parent.after(self.dragElement);
								} else {
									if (parent == self.dragElement.parenNode) {
										parent.appendChild(self.dragElement);
									} else {
										parent.append(self.dragElement);
									}
								}

								prepend = true;
							} else {
								if (noChildren[parentTagName] || isBlock || isVattribute) { 
									parent.parentNode.insertBefore(self.dragElement, parent);
								} else {
									parent.prepend(self.dragElement);
								}

								prepend = false;
							};
							
							if (self.designerMode) {
								let parentOffset = offset(self.dragElement.offsetParent);
								self.dragElement.style.position =  "absolute"; 
								self.dragElement.style.x = x - (parentOffset.left - self.frameDoc.scrollLeft); 
								self.dragElement.style.y = y - (parentOffset.top - self.frameDoc.scrollTop); 			
							}
							
					} catch(err) {
						console.log(err);
						return false;
					}
					
					if (!self.designerMode && self.iconDrag) {
						self.iconDrag.style.top  = (y + 60) + "px"; 
						self.iconDrag.style.left = (x + self.leftPanelWidth + 10) + "px"; 			
					}
				}// else //uncomment else to disable parent highlighting when dragging
				{
					//if text editor is open check if the highlighted element is not inside the editor
					if (Vvveb.WysiwygEditor.isActive )  {
						if (self.texteditEl.contains(event.target)) {
							return true;
						}
					}
						 
					document.getElementById("highlight-box").setAttribute("style",
						`top:${pos.top - (self.frameDoc.scrollTop ?? 0)}px; 
						 left:${pos.left - (self.frameDoc.scrollLeft ?? 0)}px;
						 width:${width}px; 
						 height:${height}px;
						 display:${event.target.hasAttribute('contenteditable') ? "none":"block"};
						 border:${self.isDragging ? "1px dashed #0d6efd":""};
					`);

					if (height < 50) {
						document.getElementById("section-actions").classList.add("outside");	 
					} else {
						document.getElementById("section-actions").classList.remove("outside");	
					}

					let elementType = self._getElementType(event.target);
					document.querySelector("#highlight-name .type").innerHTML = elementType[0];
					document.querySelector("#highlight-name .name").innerHTML = elementType[1];
				}
			}	
			
		};
		
		self.frameBody.addEventListener("mousemove", highlightMove);
		
		let highlightUp = function(event) {
			self.isResize = false;
			document.querySelectorAll("#section-actions, #highlight-name").forEach(el => el.style.display = "");
			if (self.isDragging) {
				self.isDragging = false;
				Vvveb.Builder.highlightEnabled = true;
				if (self.iconDrag) self.iconDrag.remove();
				document.getElementById("component-clone")?.remove();

				if (self.dragMoveMutation === false) {				
					if (self.component.dragHtml || Vvveb.dragHtml) { //if dragHtml is set for dragging then set real component html
						if (self.component) {
							let html = self.component.html.replace('RANDOM_ID', Math.floor(Math.random() * 1000));
							newElement = generateElements(html)[0];
							self.dragElement.replaceWith(newElement);
							self.dragElement = newElement;
						}
					} 
					
					if (self.component.afterDrop) self.dragElement = self.component.afterDrop(self.dragElement);
				} else {
					self.selectedEl.classList.remove("is-dragged");
					self.dragElement.replaceWith(self.selectedEl);
					self.dragElement = self.selectedEl;
				}

				const node = self.dragElement;
				self.selectNode(node);
				Vvveb.TreeList.loadComponents();
				Vvveb.TreeList.selectComponent(node);
				self.loadNodeComponent(node);
				
				if (self.dragType == "section") {
					node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
				}

				if (self.dragMoveMutation === false) {
					Vvveb.Undo.addMutation({type: 'childList', 
											target: node.parentNode, 
											addedNodes: [node], 
											nextSibling: node.nextSibling});
				} else {
					self.dragMoveMutation.newParent = node.parentNode;
					self.dragMoveMutation.newNextSibling = node.nextSibling;
					
					Vvveb.Undo.addMutation(self.dragMoveMutation);
					self.dragMoveMutation = false;
				}
			}
		};
		
		self.frameBody.addEventListener("mouseup", highlightUp);

		let highlightDbClick = function(event) {
			
			if (Vvveb.Builder.isPreview == false && Vvveb.Builder.highlightEnabled) {
				
				if (!Vvveb.WysiwygEditor.isActive)  {
					self.selectPadding = 10;
					self.texteditEl = target = event.target;

					Vvveb.WysiwygEditor.edit(self.texteditEl);
					
					_updateSelectBox = function(event) {
						if (!self.texteditEl) return;
						let pos = offset(self.selectedEl);

						let SelectBox = document.getElementById("select-box");

						SelectBox.style.top  = (pos.top - (self.frameDoc.scrollTop ?? 0)  - self.selectPadding) + "px";
						SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px";
						SelectBox.style.width = (self.texteditEl.offsetWidth + (self.selectPadding * 2)) + "px";
						SelectBox.style.height = (self.texteditEl.offsetHeight + (self.selectPadding * 2)) + "px";
						SelectBox.style.display = "block";
					};
					
					//update select box when the text size is changed
					self.texteditEl.addEventListener("blur", _updateSelectBox);	
					self.texteditEl.addEventListener("keyup", _updateSelectBox);	
					self.texteditEl.addEventListener("paste", _updateSelectBox);	
					self.texteditEl.addEventListener("input", _updateSelectBox);	
					_updateSelectBox();	
					
					document.getElementById("select-box").classList.add("text-edit")
					document.getElementById("select-actions").style.display = "none";
					document.getElementById("highlight-box").style.display = "none";
				}
		 	}
		};
		
		self.frameBody.addEventListener("dblclick", highlightDbClick);
		
		let highlightClick = function(event) {
			
			if (Vvveb.Builder.isPreview == false && Vvveb.Builder.highlightEnabled){
				if (event.target) {
					if (Vvveb.WysiwygEditor.isActive )  {
						if (self.texteditEl.contains(event.target)) {
							return true;
						}
					}
					
					self.selectNode(event.target);
					Vvveb.TreeList.selectComponent(event.target);
					self.loadNodeComponent(event.target);

					if (Vvveb.component.resizable) {
						document.getElementById("select-box").classList.add("resizable");
                      	self.resizeMode = Vvveb.component.resizeMode;
					} else {
						document.getElementById("select-box").classList.remove("resizable");
					}
					
					document.getElementById("add-section-box").style.display = "none";
					event.preventDefault();
					return false;
				}	
			}	
			
		};
		
		self.frameBody.addEventListener("click", highlightClick);
		
	},
	
	_initBox: function() {
		let self = this;
		
		document.getElementById("drag-btn").addEventListener("mousedown", function(event) {
			//self.dragElement = self.selectedEl.setAttribute("style",Vvveb.dragElementStyle);
			if (event.which == 1) {//left click
				self.isDragging = true;
				document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = "");
				
				
				if (self.designerMode) {
					self.dragElement = self.selectedEl;
				} else {
					self.selectedEl.style.position  = ""; 
					self.selectedEl.style.top  = ""; 
					self.selectedEl.style.left = ""; 			

					self.selectedEl.classList.add("is-dragged");
					self.dragElement = generateElements(Vvveb.dragHtml)[0];
				}

				const node = self.selectedEl;			

				self.dragMoveMutation = {type: 'move', 
									target: node,
									oldParent: node.parentNode,
									oldNextSibling: node.nextSibling};
					
				//self.selectNode(false);
				event.preventDefault();
				return false;
			}
		});


		let resizeDown = function(event) {
			if (event.which == 1) {//left click
				document.querySelector("#section-actions, #highlight-name, #highlight-box").style.display = "none";
				
				self.isResize = true;
				self.initialSize = {"width" : self.selectedEl.offsetWidth, "height" : self.selectedEl.offsetHeight};
				self.initialPosition = false;
				self.resizeHandler = this.className;

				event.preventDefault();
				return false;
			}
		};
		
		document.querySelectorAll(".resize > div").forEach(e => e.addEventListener("mousedown", resizeDown));

		document.getElementById("down-btn").addEventListener("click", function(event) {

			document.getElementById("select-box").style.display = "none";

			Vvveb.Builder.moveNodeDown();

			event.preventDefault();
			return false;
		});
		
		document.getElementById("up-btn").addEventListener("click", function(event) {
			document.getElementById("select-box").style.display = "none";

			Vvveb.Builder.moveNodeUp();

			event.preventDefault();
			return false;
		});
		
		document.getElementById("clone-btn").addEventListener("click", function(event) {
			
			Vvveb.Builder.cloneNode();
			
			event.preventDefault();
			return false;
		});
		
		document.getElementById("parent-btn").addEventListener("click", function(event) {
			
			const node = self.selectedEl.parentNode;
			
			self.selectNode(node);
			self.loadNodeComponent(node);
			Vvveb.TreeList.selectComponent(node);
			
			event.preventDefault();
			return false;
		});		
		
		document.getElementById("save-reusable-btn").addEventListener("click", function(event) {
			
			const node = self.selectedEl;

			let type = 'block';
			if (node.tagName.toLowerCase() == 'section') {
				type = 'section';
			}
			
			const name = prompt("Enter name for new reusable " + type, '');
			if (name) {
				Vvveb.Builder.saveElement(node, type, name);
			}
			
			event.preventDefault();
			return false;
		});
		
		let codeEditorOldValue;
		document.getElementById("edit-code-btn").addEventListener("click", function(event) {
			let value = Vvveb.Builder.selectedEl.innerHTML;

			Vvveb.ModalCodeEditor.show();
			Vvveb.ModalCodeEditor.setValue(value);
			
			codeEditorOldValue = value;

			event.preventDefault();
			return false;
		});

		let onSave = function(event) {
			Vvveb.Builder.selectedEl.innerHTML = event.detail;
			
			const node = Vvveb.Builder.selectedEl;
			Vvveb.Undo.addMutation({type:'characterData', 
				target: node, 
				oldValue: codeEditorOldValue, 
				newValue: node.innerHTML});				
				
			Vvveb.Builder.selectNode(node);	
		};
		
		window.addEventListener("vvveb.ModalCodeEditor.save", onSave); 
		
		document.getElementById("translate-code-btn")?.addEventListener("click", function(event) {
			let text = Vvveb.Builder.selectedEl.innerHTML.trim();
			// uncomment to use outerHTML, not recommended
			//let text = selectedEl.outerHTML;
			
			fetch(namespaceUrl + "/translate&action=get", {method: "POST",  body: new URLSearchParams({text, csrf: document.getElementById('csrf')?.value}), credentials: 'include'})
			.then((response) => {
				if (!response.ok) { throw new Error(response) }
				return response.json()
			})
			.then((data) => {
				for (code in data) {
					let translation = data[code];
					document.querySelector("#lang-" + code + "-editor textarea").value = translation;
				}
			})
			.catch(error => {
				console.log(error.statusText);
				displayToast("danger", "Error", "Error loading translations!");
			});			

			
			Vvveb.ModalTranslateEditor.show();
			Vvveb.ModalTranslateEditor.setValue(text);

			
			event.preventDefault();
			return false;
		});

		let onTranslateSave = function(event) { 
			let data     = Object.fromEntries(new FormData(document.getElementById("translateForm")));
			data["csrf"] = document.getElementById('csrf')?.value;

			fetch(namespaceUrl + "/translate&action=save", {method: "POST",  body: new URLSearchParams(data)})
			.then((response) => {
				if (!response.ok) { throw new Error(response) }
				return response.json()
			})
			.then((data) => {
				let bg = "success";
				
				if (data.success || text == "success") {		
				} else {
					bg = "danger";
				}
				
				displayToast(bg, "Save", data.message ?? data);		
			})
			.catch(error => {
				console.log(error.statusText);
				displayToast("danger", "Error", "Error saving translations!");
			});
			/*
			Vvveb.Builder.frameBody.querySelectorAll("form").forEach(f => {
				console.log( JSON.stringify(Object.fromEntries(new FormData(f))) ); 
			});				
			*/
			Vvveb.Builder.selectedEl.innerHTML = event.detail;
			//selectedEl.outerHTML = text;
		};

		//window.removeEventListener("vvveb.ModalTranslateEditor.save", onTranslateSave); 
		window.addEventListener("vvveb.ModalTranslateEditor.save", onTranslateSave); 
		
			
		document.getElementById("delete-btn").addEventListener("click", function(event) {
			document.getElementById("select-box").style.display = "none";
			
			const node = self.selectedEl;
		
			Vvveb.Undo.addMutation({type: 'childList', 
				target: node.parentNode, 
				removedNodes: [node],
				nextSibling: node.nextSibling});

			self.selectedEl.remove();
			Vvveb.TreeList.loadComponents();
			Vvveb.SectionList.loadSections();
			
			event.preventDefault();
			return false;
		});

		let addSectionBox = document.getElementById("add-section-box");
		let addSectionElement;
		
		document.getElementById("add-section-btn").addEventListener("click", function(event) {
			
			addSectionElement = self.highlightEl; 
			addSectionBox.style.display  = "block"; 
			addSectionBox.classList.remove("only-base"); 
			let tagName = self.highlightEl.tagName.toLowerCase();
			let bsTab;
			if (["section", "footer", "header"].includes(tagName)) {
				addSectionBox.classList.add("only-sections"); 
				bsTab = bootstrap.Tab.getOrCreateInstance(document.getElementById("box-sections-tab"));
			} else {
				addSectionBox.classList.remove("only-sections"); 
				bsTab = bootstrap.Tab.getOrCreateInstance(document.getElementById("box-components-tab"));
			}
			bsTab.show(); 

			let pos = offset(addSectionElement);	
			let top = ((pos.top + window.FrameWindow.pageYOffset + addSectionElement.clientTop) - self.frameHtml.scrollTop) + addSectionElement.offsetHeight;
			let left = ((pos.left + window.FrameWindow.pageXOffset + addSectionElement.clientLeft) - self.frameHtml.scrollLeft) + (addSectionElement.offsetWidth / 2) - (addSectionBox.offsetWidth / 2);
			let outerHeight = window.FrameWindow.innerHeight + self.frameHtml.scrollTop;

			if ((left + addSectionBox.offsetWidth) > self.frameHtml.offsetWidth) left = self.frameHtml.offsetWidth - addSectionBox.offsetWidth;
			if (((top + addSectionBox.offsetHeight) + self.frameHtml.scrollTop) > outerHeight) top = top - addSectionBox.offsetHeight;
			//check if box is out of viewport and move inside
			if (left < 0) left = 0;
			if (top < 0) top = 0;
			
			addSectionBox.style.top  = top + "px"; 
			addSectionBox.style.left = left + "px"; 

			event.preventDefault();
			return false;
		});
		
		document.getElementById("close-section-btn").addEventListener("click", function(event) {
			addSectionBox.style.display = "none";
		});
		
		function addSectionComponent(component, after = true) {
			let html    = component.html.replace('RANDOM_ID', Math.floor(Math.random() * 1000))
			let node    = generateElements(html)[0];
			let tagName = node.tagName.toLowerCase();
			let element = addSectionElement;
			
			//if section add after current section
			if (["section", "footer", "header", "nav"].includes(tagName)) {
				if (!element) {
				element = Vvveb.Builder.frameBody.querySelector(':scope > ' + tagName + ':last-of-type') || Vvveb.Builder.frameBody.querySelector(':scope > section:last-of-type') || Vvveb.Builder.frameBody.querySelector(':scope > header:last-of-type') || Vvveb.Builder.frameBody.querySelector(':scope > main') || Vvveb.Builder.frameBody.querySelector(':scope > footer:last-of-type');
				} else {
					while (element = element.parentElement) {
						tagName = element.tagName.toLowerCase();
						if (["section", "footer", "header", "nav"].includes(tagName)) {
							after = true;
							break;
						}					
					} 
				} 
				
				if (!element) {
					element = addSectionElement;
				}
			}
			
			if (element) {
				if (after) {
					element.after(node);
				} else {
					element.append(node);
				}
			} else {
				let newsection = Vvveb.Builder.frameBody.querySelector('newsection')
				if (newsection) {
					newsection.before(node);
				} else {
					Vvveb.Builder.frameBody.append(node);
				}
			}
			
			if (component.afterDrop) {
				node = component.afterDrop(node);
			}

			Vvveb.Server.renderComponents(node);
			self.selectNode(node);
			self.loadNodeComponent(node);
			Vvveb.TreeList.loadComponents();
			Vvveb.TreeList.selectComponent(node);
			
			node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});

			Vvveb.Undo.addMutation({type: 'childList', 
									target: node.parentNode, 
									addedNodes: [node], 
									nextSibling: node.nextSibling});
		}
		
		addSectionBox.addEventListener("click", function(event) {
			let element = event.target.closest("ol li");
			if (element) {
				let itemType = element.dataset.dragType;
				let html;
				switch (itemType) {
					case 'component':
						html = Vvveb.Components.get(element.dataset.type);
					break;					
					case 'block':
						html = Vvveb.Blocks.get(element.dataset.type);
					break;					
					case 'section':
						html = Vvveb.Sections.get(element.dataset.type);
					break;
				}
				

				addSectionComponent(html, (document.querySelector("[name='add-section-insert-mode']:checked").value == "after"));

				addSectionBox.style.display = "none";
			}
		});
		
	},	

/* drag and drop */
	_initDragdrop : function() {

		let self = this;
		self.isDragging = false;	
		
		document.addEventListener("mousedown", function(event) {
			let element = event.target.closest(".drag-elements-sidepane ul > li > ol > li[data-drag-type]");
			let html;
			
			if (element && event.which == 1) {//left click
				document.getElementById("component-clone")?.remove();
				document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(e => e.style.display = "none");
				
				self.dragType  = element.dataset.dragType;
				if (self.dragType == "component") {
					self.component = Vvveb.Components.get(element.dataset.type);
				}
				else if (self.dragType == "section") {
					self.component = Vvveb.Sections.get(element.dataset.type);
				}
				else if (self.dragType == "block") {
					self.component = Vvveb.Blocks.get(element.dataset.type);
				}
				
				if (self.component.dragHtml) {
					html = self.component.dragHtml;
				} else if (Vvveb.dragHtml) { 
					html = Vvveb.dragHtml;
				} else {
					html = self.component.html.replace('RANDOM_ID', Math.floor(Math.random() * 1000));
				}
				
				self.dragElement = generateElements(html)[0];
				//self.dragElement.css("border", "1px dashed #4285f4");
				
				if (self.component.dragStart) self.dragElement = self.component.dragStart(self.dragElement);

				self.isDragging = true;
				if (Vvveb.dragIcon == 'html') {
					self.iconDrag = generateElements(html)[0];
					self.iconDrag.setAttribute("id", "dragElement-clone");
					self.iconDrag.style.position = "absolute";
				}
				else if (self.designerMode == false) {
					self.iconDrag = document.createElement("img");
					self.iconDrag.setAttribute("id", "dragElement-clone");
					self.iconDrag.setAttribute("src", element.style.backgroundImage.replace(/^url\(['"](.+)['"]\)/, '$1'));
					
					self.iconDrag.style.zIndex = "100";
					self.iconDrag.style.position = "absolute";
					self.iconDrag.style.width = "64px";
					self.iconDrag.style.height = "64px";
					self.iconDrag.style.top = event.y + "px";
					self.iconDrag.style.left = event.x + "px";
				}
					
				document.body.append(self.iconDrag);
				
				event.preventDefault();
				return false;
			}
		});
		
		document.addEventListener('mouseup', function(event) {
			if (self.iconDrag && self.isDragging == true) {
				self.isDragging = false;
				document.getElementById("component-clone")?.remove();
				document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = "");
				self.iconDrag.remove();
				if(self.dragElement){
					self.dragElement.remove();
				}
			}
		});
		
		document.addEventListener('mousemove', function(event) {
			if (self.iconDrag && self.isDragging == true) {
				let x = (event.clientX || event.clientX);
				let y = (event.clientY || event.clientY);

				self.iconDrag.style.left  = (x - 60) + "px";
				self.iconDrag.style.top  = (y - 30) + "px";

				const elementMouseIsOver = document.elementFromPoint(x - 60, y - 40);
				
				//if drag elements hovers over iframe switch to iframe mouseover handler	
				return;
				if (elementMouseIsOver && elementMouseIsOver.tagName == 'IFRAME') {
					self.frameBody.dispatchEvent(new MouseEvent("mousemove", {
								bubbles: true,
								cancelable: true,
					}));					
					
					//self.frameBody.trigger("mousemove", event);
					event.stopPropagation();
					self.selectNode(false);
				}
			}
		});
		
		document.addEventListener("mouseup", function(event) {
			let element = event.target.closest(".drag-elements-sidepane ul > ol > li > li");
			if (element) {
				self.isDragging = false;
				document.getElementById("component-clone")?.remove()
				document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = "");
			}
		});
			
	},
	
	removeHelpers: function (html, keepHelperAttributes = false) {
		//tags like stylesheets or scripts 
		html = html.replace(/<[^>]+?data-vvveb-helpers.+?>/gi, "");
		html = html.replace(/<[^>]+?vvvebjs-new-section.+?>.+?<\/newsection>/gims, "");
		//attributes
		if (!keepHelperAttributes) {
			html = html.replace(/\s*data-vvveb-\w+(=["'].*?["'])?\s*/gi, "");
		}
		
		html = html.replaceAll("vvveb-hidden", "").replaceAll("data-vvvebjs-editor", "");
		return html;
	},

	getHtml: function(keepHelperAttributes = true, filter = true) {
		let doc = window.FrameDocument;
		let hasDoctpe = (doc.doctype !== null);
		let html = "";
		
		doc.querySelectorAll("[contenteditable]").forEach(e => e.removeAttribute("contenteditable"));
		doc.querySelectorAll("[spellcheckker]").forEach(e => e.removeAttribute("spellcheckker"));
		doc.querySelectorAll('script[src^="chrome-extension://"]').forEach(e => e.remove());
		doc.querySelectorAll('script[src^="moz-extension://"]').forEach(e => e.remove());
		
		// scroll page to top to avoid saving the page in a different state
		// like saving with sticky classes set for navbar etc
		// this.iframe.contentWindow.scrollTo(0,0);
		if (filter) {
			window.dispatchEvent(new CustomEvent("vvveb.getHtml.before", {detail: doc}));
		}

		if (hasDoctpe) html =
		"<!DOCTYPE "
		+ doc.doctype.name
		+ (doc.doctype.publicId ? ' PUBLIC "' + doc.doctype.publicId + '"' : '')
		+ (!doc.doctype.publicId && doc.doctype.systemId ? ' SYSTEM' : '') 
		+ (doc.doctype.systemId ? ' "' + doc.doctype.systemId + '"' : '')
		+ ">\n";
          
		Vvveb.FontsManager.cleanUnusedFonts();

		html += doc.documentElement.outerHTML;
		html = this.removeHelpers(html, keepHelperAttributes);

		if (filter) {
			 window.dispatchEvent(new CustomEvent("vvveb.getHtml.after", {detail: doc}));
			 window.dispatchEvent(new CustomEvent("vvveb.getHtml.filter", {detail: html}));
		}
         
         return html;
	},
	
	setHtml: function(html) {
		//documentElement.innerHTML resets <head> each time and the page flickers
		//return window.FrameDocument.documentElement.innerHTML = html;
		
		function getTag(html, tag, outerHtml = false) {
			const start = html.indexOf("<" + tag);
			const end = html.indexOf("</" + tag);		

			if (start >= 0 && end >= 0) {
				if (outerHtml)
					return html.slice(start, end + 3 + tag.length);
				else
					return html.slice(html.indexOf(">", start) + 1, end);
			} else {
				return html;
			}
		}

		if (this.runJsOnSetHtml) {
			this.frameBody.innerHTML = getTag(html, "body");
		}
		else {
			window.FrameDocument.body.innerHTML = getTag(html, "body");
		}
			
		//use outerHTML if you want to set body tag attributes
		//window.FrameDocument.body.outerHTML = getTag(html, "body", true);

		//set head html only if changed to avoid page flicker
		let headHtml = getTag(html, "head");
		if (window.FrameDocument.head.innerHTML != headHtml) {
			window.FrameDocument.head.innerHTML = headHtml;
		}
	},

	saveElement: function(element, type, name, callback) {
		if (type == 'section') {
			Vvveb.Sections.add('reusable/'+ name, {
				name,
				image: "img/logo-small.png",
				html: element.outerHTML});
			
			if (Vvveb.SectionsGroup["Reusable"] === undefined) {
				Vvveb.SectionsGroup["Reusable"] = [];
			}
			
			Vvveb.SectionsGroup["Reusable"].push('reusable/'+ name);
			Vvveb.Builder.loadSectionGroups();
		} else {
			Vvveb.Blocks.add('reusable/'+ name, {
				name,
				image: "img/logo-small.png",
				html: element.outerHTML});
			
			if (Vvveb.BlocksGroup["Reusable"] === undefined) {
				Vvveb.BlocksGroup["Reusable"] = [];
			}
			
			Vvveb.BlocksGroup["Reusable"].push('reusable/'+ name);
			Vvveb.Builder.loadBlockGroups();
		}
		
		let data = {type, name, html:element.outerHTML, csrf: document.getElementById('csrf')?.value};
		
		fetch(saveReusableUrl, {method: "POST",  body: new URLSearchParams(data), credentials: 'include'})
		.then((response) => {
			if (!response.ok) { throw new Error(response) }
			return response.json()
		})
		.then((data) => {
			if (callback) callback(data);
			let bg = "success";
			if (data.success || text == "success") {		
			} else {
				bg = "danger";
			}
			
			displayToast(bg, "Save", data.message ?? data);					
		})
		.catch(error => {
			console.log(error.statusText);
			displayToast("danger", "Error", "Error saving!");
		});
	},
	
	saveAjax: function(data, saveUrl, callback, error ) {	
		if (!data) data = Vvveb.FileManager.getPageData();
		
		if (!data.hasOwnProperty("file")) {
			data["file"]  = Vvveb.FileManager.getCurrentFileName();
		}

		if (!data.hasOwnProperty("url")) {
			data["url"]  = Vvveb.FileManager.getPageData('url');
		}

		data["elements"] = Vvveb.ChangeManager.getChangedElements();
		data["csrf"]     = document.getElementById('csrf')?.value;
                
		if (!data.hasOwnProperty("startTemplateUrl")) {
			data["html"] = this.getHtml();
		}
		
		//data['elements'] = new URLSearchParams(data['elements']);

		return fetch(saveUrl, {
			method: "POST",  
			headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
			body:  nestedFormData(data),
			credentials: 'include'
		})
		.then((response) => {
			if (!response.ok) {
				return Promise.resolve(response.text()).then((responseInText) => {
					return Promise.reject([response, responseInText]);
				});
			}
			return response.json();
		})
		.then((data) => {
			if (callback) callback(data);
			Vvveb.Undo.reset();
			document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
			window.dispatchEvent(new CustomEvent("vvveb.Builder.saveAjax", {
				detail: data,
			}));
		})
		.catch((err) => {
			if (error) error(err);
			
			let message = err;
			try {
				let [response, responseInText] = err;
				message = responseInText ?? response.statusText ?? "Error uploading!";
			} catch (e) {
			}

			if (err.hasOwnProperty('text')) err.text().then( errorMessage => {
			  	let message = errorMessage.substr(0, 200);
				displayToast("danger", "Error", message);
			});

			if (typeof message == "string") {
				displayToast("danger", "Error", message.substr(0, 200));
			}
		});
	},
	
	setDesignerMode: function(designerMode = false) {
		this.designerMode = designerMode;
	}

};

Vvveb.ModalCodeEditor = {
	modal: false,
	modalId: 'codeEditorModal',
	name: 'ModalCodeEditor',
	editor: false,
	
	init: function(modal = false, editor = false) {
		if (modal) {
			this.modal = modal;
		} else {
			this.modal = document.getElementById(this.modalId);
		}
		if (editor) {
			this.editor = editor;
		} else {
			this.editor = this.modal.querySelector('textarea');
		}
		
		let self = this;

		this.modal.querySelector('.save-btn').addEventListener("click",  function(event) {
			window.dispatchEvent(new CustomEvent("vvveb." + self.name + ".save", {detail: self.getValue()}));
			self.hide();
			return false;
		});
	},
	
	show: function(value) {
		if (!this.modal) {
			this.init();
		}
		
		const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal);
		return bsModal.show(); 
	},

	hide: function(value) {
		const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal);
		return bsModal.hide(); 
	},
	
	getValue: function() {
		return this.editor.value;;
	},
	
	setValue: function(value) {
		if (!this.modal) {
			this.init();
		}
		//enable save button
		document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled"));
		this.editor.value = value;
	},
}

//Vvveb.ModalTranslateEditor = structuredClone(Vvveb.ModalCodeEditor);
Vvveb.ModalTranslateEditor = Object.assign({}, Vvveb.ModalCodeEditor);
Vvveb.ModalTranslateEditor.modalId = 'translateEditorModal';
Vvveb.ModalTranslateEditor.name = 'ModalTranslateEditor';

Vvveb.CodeEditor = {
	
	isActive: false,
	oldValue: '',
	doc:false,
	textarea:false,
	
	init: function(doc) {
		this.textarea = document.querySelector("#vvveb-code-editor textarea");
		this.textarea.value = Vvveb.Builder.getHtml();

		this.textarea.addEventListener("keyup", e => {
			delay(() => Vvveb.Builder.setHtml(this.value), 1000);
		});

		//load code on document changes
		Vvveb.Builder.frameBody.addEventListener("vvveb.undo.add", () => Vvveb.CodeEditor.setValue());
		Vvveb.Builder.frameBody.addEventListener("vvveb.undo.restore", () => Vvveb.CodeEditor.setValue());
		
		//load code when a new url is loaded
		Vvveb.Builder.documentFrame.addEventListener("load", () => Vvveb.CodeEditor.setValue());

		this.isActive = true;
	},

	setValue: function(value) {
		if (this.isActive) {
			this.textarea.value = Vvveb.Builder.getHtml();
		}
	},

	destroy: function(element) {
		//this.isActive = false;
	},

	toggle: function() {
		if (this.isActive != true) {
			this.isActive = true;
			return this.init();
		}
		this.isActive = false;
		this.destroy();
	}
}


Vvveb.CssEditor = {
	
	isActive: false,
	oldValue: '',
	doc:false,
	textarea:false,
	
	init: function(doc) {
		this.textarea = document.getElementById("css-editor")
		this.textarea.value = Vvveb.StyleManager.getCss();
		let self = this;
		/*
		document.querySelectorAll('[href="#csscode-tab"]').forEach( t => t.addEventListener("click", e => {
			self.textarea.value = Vvveb.StyleManager.getCss();
		}));
		*/
		this.textarea.addEventListener("keyup", e => {
			delay(() => Vvveb.StyleManager.setCss(self.textarea.value), 1000);
		});
	},

	getValue: function() {
		return this.textarea.value;
	},
	
	setValue: function(value, updateStyles = true) {
		this.textarea.value = value;
		if (updateStyles) {
			Vvveb.StyleManager.setCss(value);
		}
	},

	destroy: function() {
	},

	toggle: function() {
		if (this.isActive != true) {
			this.isActive = true;
			return this.init();
		}
		this.isActive = false;
		this.destroy();
	}	
}

function displayToast(type, title, message, position = 'bottom', id = null) {
	if (!id) {
		id = position + "-toast";
	}
	
	let toast = document.getElementById(id);
	let header = toast.querySelector(".toast-header");
	toast.classList.remove("bottom-0", "top-0");
	toast.classList.add(position + "-0");
	toast.querySelector(".toast-body .message").innerHTML = message;
	header.classList.remove("danger", "success");
	header.classList.add("bg-" + type);	
	header.querySelector("strong").textContent = title;
	let toastDisplay = toast.cloneNode(true);
	toast.parentNode.appendChild(toastDisplay);
	
	let delay = 3000;
	if (type == "danger") {
		delay = 10000;
	}

	let bsToast = new bootstrap.Toast(toastDisplay, {animation:true, delay});
	toastDisplay.addEventListener('hidden.bs.toast', () => {
		toastDisplay.remove();
	});
    bsToast.show();
}		

Vvveb.Gui = {
	
	init: function() {
		document.querySelectorAll("[data-vvveb-action]").forEach(function (el,i) {
			const on = el.dataset.vvvebOn ?? "click";
			el.addEventListener(on, Vvveb.Gui[el.dataset.vvvebAction]);
		});
		
		this.shortcuts();	
	},
	
	shortcuts: function() {
		let self = this;

		handleShortcuts = function(e) {
			if (e.ctrlKey) {
				switch (e.key) {
					case 's':
						e.preventDefault();
						let btn = document.querySelector('.save-btn');
						let url = btn.dataset.vvvebUrl;
						self.saveAjax(null, url, document.querySelector('.save-btn'));
						return;
					case 'z':
						e.preventDefault();
						self.undo();
						return;
					case 'Z':
					case 'y':
						e.preventDefault();
						self.redo();
						return;
					case 'L':
						e.preventDefault();
						self.toggleTreeList();
						return;
					case 'e':
						e.preventDefault();
						self.toggleEditor();
						return;
					case 'P':
						e.preventDefault();
						self.newPage();
						return;					
					case 'S':
						e.preventDefault();
						self.newSection();
						return;
				}
			}
		}
		
		//handle shortcuts from main window and iframe also
		document.addEventListener('keydown', handleShortcuts);		
		window.addEventListener('vvveb.iframe.loaded', () => {
			Vvveb.Builder.frameBody.addEventListener('keydown', handleShortcuts);		
		});

	},
	
	undo : function () {
		if (Vvveb.WysiwygEditor.isActive) {
			Vvveb.WysiwygEditor.undo();
		} else {
			Vvveb.Undo.undo();
		}
		Vvveb.Builder.selectNode();
	},
	
	redo : function () {
		if (Vvveb.WysiwygEditor.isActive) {
			Vvveb.WysiwygEditor.redo();
		} else {
			Vvveb.Undo.redo();
		}
		Vvveb.Builder.selectNode();
	},
	
	//show modal with html content
	save : function () {
		document.getElementById('textarea-modal textarea').value = Vvveb.Builder.getHtml();
		document.getElementById('textarea-modal').modal();
	},
    
	//show offcanvas with page changes
	showChanges : function () {
		let base, folder, filename, page;
		filename = Vvveb.FileManager.getPageData('file');
		base     =  (base = /[^\/]+$/.exec(filename)) ? base[0] : '';
		folder   =  (folder = /(.+)\//.exec(filename)) ? folder[1] : '/';

		page 	 = Vvveb.FileManager.getCurrentUrl();
		page     = (page = /[^\/]+\?$/.exec(page)) ? page[0] : '';
		page     = page.replace(".html", "");
		page 	 = page ? page : "new-template";
		page    += ".html";

		let safeOffcanvas = document.querySelector("#save-offcanvas");
		let element;
		
		if (element = safeOffcanvas.querySelector("[data-v-filename]")) {
			element.textContent = filename;
		}
		if (element = safeOffcanvas.querySelector("input[data-v-filename]")) {
			element.value = filename;
		}
		if (element = safeOffcanvas.querySelector("[data-v-basename]")) {
			element.value = page;
		}
		if (element = safeOffcanvas.querySelector("[data-v-theme-folders]")) {
			element.value = folder;
		}
		
		Vvveb.ChangeManager.render();
	},
	
	//post html content through ajax to save to filesystem/db
	saveAjax : function (event, saveUrl = null, saveBtn = null) {
		let btn = saveBtn ?? this;
		saveUrl = saveUrl ?? this.dataset.vvvebUrl;
		let pageData = Vvveb.FileManager.getPageData();
		//if offcanvas check if user provided new template name
		if (btn.classList.contains("save-offcanvas")) {
			if (document.querySelector("#save-offcanvas [name=template]:checked").value == "new") {
				pageData['file'] = document.querySelector("#save-offcanvas [name=folder]").value + "/" + document.querySelector("#save-offcanvas [name=file]").value;
			}
		}

		btn.querySelector(".loading").classList.remove("d-none");
		btn.querySelector(".button-text").classList.add("d-none");
	
		return Vvveb.Builder.saveAjax(pageData, saveUrl, (data) => {
			//use toast to show save status
			let success = true;
			let bg = "success";
			
			if (data.success || data == "success") {		
			} else {
				bg = "danger";
			}
			
			if (typeof data.message === "string") {
				displayToast(bg, "Save", data.message ?? data);
			} else if (Array.isArray(data.message)) {
				for (let message of data.message) {

					if (message.success || data == "success") {	
						bg = "success"
					} else {
						bg = "danger";
						success = false;
					}
					
					displayToast(bg, "Save", message.message);
				}
			} else if (typeof data === "string") {
				displayToast(bg, "Save", data);
			}

			const offcanvas = document.getElementById('save-offcanvas');
			if (offcanvas) {
				let instance = bootstrap.Offcanvas.getInstance(offcanvas);
				if (instance) instance.hide();			
			}
			
			btn.querySelector(".loading").classList.add("d-none");
			btn.querySelector(".button-text").classList.remove("d-none");
			
			if (success) {
				document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
			}
			
		}, (error) => {
			btn.querySelector(".loading").classList.add("d-none");
			btn.querySelector(".button-text").classList.remove("d-none");
			let message = error?.statusText ?? "Error saving!";
			displayToast("danger", "Error", message);
		});		
	},
	
	download : function () {
		const filename = /[^\/]+$/.exec(Vvveb.Builder.iframe.src)[0];
		const uriContent = "data:application/octet-stream,"  + encodeURIComponent(Vvveb.Builder.getHtml());

		let link = document.createElement('a');
		if ('download' in link) {
			link.dataset.download = filename;
			link.href = uriContent;
			link.target = "_blank";
			
			document.body.appendChild(link);
			const result = link.click();
			document.body.removeChild(link);
			link.remove();
			
		} else {
			location.href = uriContent;
		}
	},
	
	viewport : function () {
		let breakpoint = this.dataset.view;
		document.getElementById("canvas").classList.remove("responsive");
		document.getElementById("iframe-wrapper").removeAttribute("style");
		document.querySelectorAll(".responsive-btns .active").forEach(e => e.classList.remove("active"));

		if (Vvveb.StyleManager.currentBreakpoint == breakpoint) {
			Vvveb.StyleManager.currentBreakpoint = "none";
			breakpoint = null;
		}
		
		Vvveb.Builder.documentFrame.style.width = "";
		Vvveb.Builder.documentFrame.style.height = "";
		
		if (breakpoint) {
			document.getElementById("canvas").classList.add("responsive");
			document.getElementById("iframe-wrapper").style.width = Vvveb.StyleManager.breakpoints[breakpoint].replace(".98", "");
			Vvveb.StyleManager.currentBreakpoint = breakpoint;
			this.classList.add("active");
		}
	},
	
	toggleEditor : function () {
		document.getElementById("vvveb-builder").classList.toggle("bottom-panel-expand");
		document.getElementById("toggleEditorJsExecute").classList.toggle("d-none");
		//hide breadcrumb when showing the editor
		document.querySelector(".breadcrumb-navigator .breadcrumb").classList.toggle("d-none");
		document.querySelector(".breadcrumb-navigator .nav-tabs").classList.toggle("d-none");
		Vvveb.CodeEditor.toggle();
		Vvveb.CssEditor.toggle();
	},
	
	toggleHidden : function () {
		Vvveb.Builder.frameBody.classList.toggle("vvveb-hidden");
	},

	toggleEditorJsExecute : function () {
		Vvveb.Builder.runJsOnSetHtml = this.checked;
	},
	
	preview : function () {
		(Vvveb.Builder.isPreview == true)?Vvveb.Builder.isPreview = false:Vvveb.Builder.isPreview = true;
		document.getElementById("iframe-layer").classList.toggle("d-none");
		document.getElementById("vvveb-builder").classList.toggle("preview");
	},
	
	fullscreen : function () {
		launchFullScreen(document); // the whole page
	},
	
	search : function () {
		let searchText = this.value;
		let panel = this.parentNode.parentNode.querySelector("div > ul");
		panel.querySelectorAll("li ol li").forEach(function (el, i) {
			el.style.display = "none";
			if (el.dataset.search.indexOf(searchText) > -1) el.style.display = "";
		});
	},
	
	clearSearch : function (e) {
		let input = this.parentNode.querySelector("input");
		input.value = "";
		input.dispatchEvent(new KeyboardEvent("keyup", {
			bubbles: true,
			cancelable: true,
		}));
	},
	
	toggleSections : function (e) {
		let icon = this.querySelector("i");
		let closed = (this.dataset.closed == "true");

		if (closed) {
			icon.setAttribute("class", "la la-minus");
			this.dataset.closed = "false";
		} else {
			icon.setAttribute("class", "la la-plus");
			this.dataset.closed = "true";
		}
		
		this.parentNode.parentNode.parentNode.querySelectorAll('input.header_check[type="checkbox"]').forEach(e => e.checked = closed);
	},

	//Pages, file/components tree 
	newPage : function () {
		
		let newPageModal = document.getElementById('new-page-modal');
		let form = newPageModal.querySelector("form");
		
		const bsModal = bootstrap.Modal.getOrCreateInstance(newPageModal);
		bsModal.show(); 

		let submitForm = function(e) {

			let data = {};
			this.querySelectorAll("input[type=text],input[type=number],input[type=checkbox]:checked,input[type=radio]:checked,input[name=image],textarea,select:not(:disabled)").forEach( (el, i) => {
				if (el.offsetParent || el.name == 'image') data[el.name] = el.value;
			});			

			if (data['file']) {
				data['title']  = data['file'].replace('/', '').replace('.html', '');
				//let name = data['name'] = data['folder'].replace('/', '_') + "-" + data['title'];
				if (!data['name']) {
					data['name'] = data['title'];
				}
				data['url']  = data['folder'] + "/" + data['file'];
				data['url']  = Vvveb.themeBaseUrl + data['url'];
			} else {
				data['className'] = 'page';
			}

			data['file']             = data['file'] ? data['file'] : '';
			data['filename']         = data['filename'] ? data['filename'] : data['file'];
			data['startTemplateUrl'] = data['startTemplateUrl'] ? data['startTemplateUrl'] : '';
			e.preventDefault();

			return Vvveb.Builder.saveAjax(data, this.action, function (savedData) {
				if (savedData?.success) {
					data.title = data.name;

					if (typeof savedData === 'object' && savedData !== null) {
						data.name = savedData.name ?? data.name;
						data.url = savedData.url ?? data.url;
						data.file = savedData.file ?? (data.folder + "/" + data.file);
						data.title = savedData.title ?? data.title;
					}
					
					delete data['startTemplateUrl'];
					let page = Vvveb.FileManager.addPage(data.name, data, false, false);
					Vvveb.FileManager.loadPage(data.name);
					Vvveb.FileManager.scrollToPage(page);
					bsModal.hide();

		
					let pageUrl = window.location.pathname + "?module=editor/editor&template=" + data["file"] + "&url=" + data["url"] + "&name=" + data["title"];
					if (data["post_id"]) {
						pageUrl += "&post_id=" + data["post_id"];
					}
					if (data["product_id"]) {
						pageUrl += "&product_id=" + data["product_id"];
					}
					
					window.history.pushState({name:data.name}, null, pageUrl); 
				} else {
					let message = savedData?.message ?? "Error saving!";
					displayToast("danger", "Error", message);
				}
			});
		};
		
		if (!form.dataset.init) {
			form.addEventListener("submit", submitForm);
			form.dataset.init = true;
		}
	},

	newSection : function (e, onlyBase = false) {
	
		let addSectionBox = document.getElementById("add-section-box");
		let addSectionElement = {};
		
		//addSectionElement = self.highlightEl; 
		addSectionBox.style.display  = "block"; 

		let bsTab;
		addSectionBox.classList.add("only-sections"); 
		if (onlyBase) {
			addSectionBox.classList.add("only-base"); 
		} else {
			addSectionBox.classList.remove("only-base");
		}
		
		bsTab = bootstrap.Tab.getOrCreateInstance(document.getElementById("box-sections-tab"));
		bsTab.show(); 

		event.preventDefault();
		return false;
			
	},
	
	setDesignerMode : function () {
		//aria-pressed attribute is updated after action is called and we check for false instead of true
		let designerMode = this.attributes["aria-pressed"].value == "true";
		Vvveb.Builder.setDesignerMode(designerMode);
	},
	
	//layout
	togglePanel: function (panel, cssVar) {
		panel = document.querySelector(panel);
		let body = document.querySelector("body");
		let prevValue = getComputedStyle(body).getPropertyValue(cssVar);
		let visible = false;
		
		if (prevValue !== "0px") {
			panel.dataset.layoutToggle = prevValue;
			body.style.setProperty(cssVar, "0px");
			panel.style.display = "none";
			visible = false;
		} else {
			prevValue= panel.dataset.layoutToggle;
			body.style.setProperty(cssVar, "");
			panel.style.display = "";
			visible = true;
		}
		
		return visible;
	},

	toggleFileManager: function () {
		Vvveb.Gui.togglePanel("#filemanager", "--builder-filemanager-height");
	},
	
	toggleLeftColumn: function () {
		Vvveb.Gui.togglePanel("#left-panel", "--builder-left-panel-width");
	},	
	
	toggleRightColumn: function (rightColumnEnabled = null) {
		rightColumnEnabled = Vvveb.Gui.togglePanel("#right-panel", "--builder-right-panel-width");

		document.getElementById("vvveb-builder").classList.toggle("no-right-panel");
		document.querySelector(".component-properties-tab").classList.toggle("d-none");
		
		Vvveb.Components.componentPropertiesElement = (rightColumnEnabled ? "#right-panel" :"#left-panel #properties") + " .component-properties";
		let componentTab = document.querySelector("#components-tab");

		if (document.getElementById("properties").offsetParent) {
			const bsTab = bootstrap.Tab.getOrCreateInstance(componentTab);
			componentTab.style.display = "";
			bsTab.show(); 
		}

	},

	toggleTreeList: function () {
		let treeList = document.getElementById("tree-list");
		treeList.classList.toggle("d-none");
		if (!treeList.offsetParent) {
			document.getElementById("toggle-tree-list").classList.remove("active");
		}
	},

	treeListRight: function () {
		let treeList = document.getElementById("tree-list");
		let btnIcon = document.querySelector("[data-vvveb-action='treeListRight'] i");
		if (treeList.style.height) {
			treeList.style.height = "";
			treeList.style.right = "";
			treeList.style.top = "";
			treeList.style.left = "";
			treeList.style.width = "";
			btnIcon.className = "icon-stop-outline";
		} else {
			treeList.style.height = "100%";
			treeList.style.right = "0";
			treeList.style.top = "0";
			treeList.style.left = "auto";
			treeList.style.width = "300px";
			btnIcon.className = "icon-remove-outline";
		}
	},
	
	treeListPanel: function () {
		let treeList = document.getElementById("tree-list");
		let canvas = document.getElementById("canvas");
		let filemanager = document.getElementById("filemanager");
		let btnIcon = document.querySelector("[data-vvveb-action='treeListPanel'] i");
		
		if (treeList.parentNode == canvas) {
			filemanager.after(treeList);
			btnIcon.className = "icon-chevron-forward-outline";
		} else {
			canvas.append(treeList);
			btnIcon.className = "icon-chevron-back-outline";
		}
	},
	
	zoomChange: function () {
		let wrapper = document.getElementById("iframe-wrapper");
		let scale = "";
		let height = "";
		if (this.value != "100") {
			scale = "scale(" + this.value + "%)";
			height = ((100 / this.value) * 100) + "%";
		}
		wrapper.style.transform = scale;
		wrapper.style.height = height;
	},

	setState: function () {
		Vvveb.StyleManager.setState(this.value);
		Vvveb.Builder.reloadComponent();
	}
}

Vvveb.StyleManager = {
	
	styles:{},
	cssContainer:false,
	breakpoints: {
		'sm':'575.98px',
		'md':'767.98px',
		'lg':'991.98px',
		'xl':'1199.98px',
		'xxl':'1399.98px'
	},
	doc:false,
	inlineCSS:false,
	currentElement:null,
	currentSelector:null,
	currentBreakpoint:"none",
	state:"",//hover, active etc
	
	init: function(doc) {
		if (doc) {
			this.doc = doc;
			
			let style = false;
			let _style = false;
			
			//check if editor style is present
			for (let i = 0; i < doc.styleSheets.length; i++) {
					_style = doc.styleSheets[i];
					if (_style.ownerNode.id && _style.ownerNode.id == "vvvebjs-styles") {
						style = _style.ownerNode;
						break;
					}
			}
			
			document.getElementById("vvvebjs-styles-css")?.remove();
			
			//if style element does not exist create it			
			if (!style) {
				style = generateElements('<style id="vvvebjs-styles"></style>')[0];
				doc.head.append(style);
				return this.cssContainer = style;
			}
			
			//if it exists
			this.cssContainer = style;
			this.loadCss();

			return this.cssContainer; 
		}
	},	
	
	loadCss: function() {
		let style = this.cssContainer.sheet;
		//if style exist then load all css styles for editor
		for (let j = 0; j < style.cssRules.length; j++) {
			let media = "none";
			
			if (style.cssRules[j].media) {
				for (breakpoint in this.breakpoints) {
					if (style.cssRules[j].media[0] === "@media (max-width: " + this.breakpoints[breakpoint] + ")") {
						media = breakpoint;
						break;
					}
				} 
			}

			style.cssRules[j].media
			
			const selector = (media === "none") ? style.cssRules[j].selectorText : style.cssRules[j].cssRules[0].selectorText;
			const styles = (media === "none") ? style.cssRules[j].style : style.cssRules[j].cssRules[0].style;

			if (media) {
				this.styles[media] = this.styles[media] ?? {};
				if (selector) {
					this.styles[media][selector] = {};
				
					for (let k = 0; k < styles.length; k++) {
								
						const property = styles[k];
						const value = styles[property];
					
						this.styles[media][selector][property] = value;
					}
				}
			}
		}		
	},
	
	getSelectorForElement: function(element) {
		if (!element) return '';
		
		let currentElement = element;
		let selector = [];
		
		while (currentElement.parentElement) {
			let elementSelector = "";
			let classSelector = Array.from(currentElement.classList).map(function (className) {
					if (Vvveb.Builder.ignoreClasses.indexOf(className) == -1) {
						return "." + className;
					}
				}).join("");

			//element (tag) selector
			let tag = currentElement.tagName.toLowerCase();
			//exclude top most element body unless the parent element is body
			if (tag == "body" && selector.length > 1) {
				break;
			}
			
			//stop at a unique element (with id)
			if (currentElement.id) {
				elementSelector = "#" + currentElement.id;
				selector.push(elementSelector);
				break;
			} else if (classSelector) {
				//class selector
				elementSelector = classSelector;
			} else {
				//element selector
				elementSelector = tag
			}
			
			if (elementSelector) {
				selector.push(elementSelector);
			}
			
			currentElement = currentElement.parentElement;
		}
		
		return selector.reverse().join(" > ");
	},	
	
	setState: function(state) {
		this.state = state;
	},

	addSelectorState: function(selector) {
		return selector + (this.state ? ":" + this.state : "");
	},
	
	setStyle: function(element, styleProp, value) {
		let selector;
		
		if (typeof(element) == "string") {
			selector = element;
		} else {
			let node = element;

			//if propert is set with inline style attribute then override it and don't save to css
			//inline text editor sets properties like font-size inline that can't be later overriten from css
			if (node.style && node.style[styleProp]) {
				node.style[styleProp] = value;
				return element;
			}

			selector = this.getSelectorForElement(node);	
		}
		
		if (this.inlineCSS) {
			element.style[styleProp] = value;
			return element;	
		}
		
		selector = this.addSelectorState(selector);
		
		const media = this.currentBreakpoint;
		
		//styles[media][selector][styleProp] = value
		if (!this.styles[media]) {
			this.styles[media] = {};
		}
		if (!this.styles[media][selector]) {
			this.styles[media][selector] = {};
		}
		if (!this.styles[media][selector][styleProp]) {
			this.styles[media][selector][styleProp] = {};
		}
		this.styles[media][selector][styleProp] = value;
		
		this.generateCss(media);

		window.dispatchEvent(new CustomEvent("vvveb.StyleManager.setStyle", {detail: {element, styleProp, value}}));
		
		return element;		
        	//uncomment bellow code to set css in element's style attribute 
		//return element.css(styleProp, value);
	},
	
	setCss: function (css) {
		this.cssContainer.innerHTML = css;
		this.loadCss();
	},

	getCss: function (css) {
		return this.cssContainer.innerHTML;
	},
	
	generateCss: function (media) {
		//let css = "";
		//for (selector in this.styles[media]) {

		//	css += `${selector} {`;
		//	for (property in this.styles[media][selector]) {
		//		value = this.styles[media][selector][property];
		//		css += `${property}: ${value};`;
		//	}
		//	css += '}';
		//}

		//this.cssContainer.innerHTML = css;

		//return element;
		//refresh container element to avoid issues with changes from code editor
		this.cssContainer = this.doc.getElementById("vvvebjs-styles");

		let css = "";
		for (media in this.styles) {
			if (media !== "none") {
				let width = this.breakpoints[media];
				css += `@media (max-width: ${width}){\n\n`
			}
			for (selector in this.styles[media]) {
				css += `${selector} {\n`;	
				for (property in this.styles[media][selector]) {
					const value = this.styles[media][selector][property];
					css += `\t${property}: ${value};\n`;
				}
				css += '}\n\n';
			}
			if (media !== "none") {
				css += `}\n\n`
			}
		}

		return this.cssContainer.innerHTML = css;
	},
	
	
	_getCssStyle: function(element, styleProp){
		let value = "", el, selector, media;

		el = element;
		if (el != this.currentElement) {
			selector = this.getSelectorForElement(el);
			this.currentElement = el;
			this.currentSelector = selector
		} else {
			selector = this.currentSelector;
		}

		selector = this.addSelectorState(selector);
		media = this.currentBreakpoint;

		if (el.style && el.style.length > 0 && el.style[styleProp]) {//check inline
			value = el.style[styleProp];
		} else if (this.styles[media] !== undefined && this.styles[media][selector] !== undefined && this.styles[media][selector][styleProp] !== undefined) {//check defined css
			value = this.styles[media][selector][styleProp];

			if (styleProp == 'font-family') {
			}
		} else if (window.getComputedStyle) {
			value = document.defaultView.getDefaultComputedStyle ? 
						document.defaultView.getDefaultComputedStyle(el,null).getPropertyValue(styleProp) : 
						window.getComputedStyle(el,null).getPropertyValue(styleProp);

		}
		
		return value;
	},
	
	getStyle: function(element,styleProp){
		return this._getCssStyle(element, styleProp);
	}
}

Vvveb.ContentManager = {
	getAttr: function(element, attrName) {
		return element.getAttribute(attrName);
	},
	
	setAttr: function(element, attrName, value) {
		return element.setAttribute(attrName, value);
	},
	
	setHtml: function(element, html, outer = false) {
		if (outer) {
			const template = document.createElement('template');
			template.innerHTML = html.trim();
			let newelement = template.content.children[0];
			element.parentNode.replaceChild(newelement,element);
			element.remove();
			return newelement;
		} else {
			element.innerHTML = html;
		}
	
		return element;
	},
	
	getHtml: function(element, outer = false) {
		if (outer) {
			return element.outerHTML;
		} else {
			return element.innerHTML;
		}
	},

	setText: function(element, text) {
		return element.textContent = text;
	},
	
	getText: function(element) {
		return element.textContent;
	},
};

function getNodeTree (node, parent, allowedComponents, idToNode = {}) {
	
	function getNodeTreeTraverse (node, parent, id = '') {
		
		if (node.hasChildNodes()) {
			for (let j = 0; j < node.childNodes.length; j++) {
				
				const child = node.childNodes[j];

				//skip text and comments nodes
				if (child.nodeType == 3 || child.nodeType == 8) {
					continue;
				}
				
				let element;
				if (child && child["attributes"] != undefined && 
					(matchChild = Vvveb.Components.matchNode(child))) {

					if (Array.isArray(allowedComponents)
						&& allowedComponents.indexOf(matchChild.type) == -1) {
						
						element = getNodeTreeTraverse(child, parent);	
						continue;
					}
				
					let title;
					for (const attr of ["id", "title", "aria-label"]) {
						title = child.getAttribute(attr);
						if (title) {
							break;
						}
					}

					element = {
						name: matchChild.name,
						image: matchChild.image,
						type: matchChild.type,
						title,
						node: child,
						id: id + '-' + j,
						children: []
					};

					element.children = [];
					parent.push(element);
					idToNode[id + '-' + j] = child;
					
					element = getNodeTreeTraverse(child, element.children, id + '-' + j);
				} else {
					element = getNodeTreeTraverse(child, parent, id + '-' + j);	
				}
			}
		}

		return false;
	}
	
	getNodeTreeTraverse(node, parent, '1');
}

function drawComponentsTree(tree) {
	let j = 1;
	let prefix = Math.floor(Math.random() * 100);
	
	function drawComponentsTreeTraverse(tree) {
		let list = document.createElement("ol");
		j++;
		
		for (i in tree) {
			let node = tree[i];
			let id = node.id;
			let li;
			
			if (!id) {
				id = prefix + '-' + j + '-' + i; 
			}
		
			let title = (node.title ? friendlyName(node.title.substr(0, 21)) : "");
			if (title) {
				title = ` - <span class="text-secondary">${title}</span>`;
			}
			
			if (tree[i].children.length > 0) {
				li = generateElements('<li data-component="' + node.name + '">\
								<label for="id' + id + '" style="background-image:url(' + Vvveb.imgBaseUrl + node.image + ')">\
									<span>' + node.name + '</span>' + title + '\
								</label>\
								<input type="checkbox" id="id' + id + '">\
							</li>')[0];		
				li.append(drawComponentsTreeTraverse(node.children));
			}
			else {
				li = generateElements('<li data-component="' + node.name + '" class="file">\
							<label for="id' +  id + '" style="background-image:url(' + Vvveb.imgBaseUrl + node.image + ')">\
								<span>' + node.name + '</span>' + title + '\
							</label>\
							<input type="checkbox" id="id' + id + '">\
							</li>')[0];
			}

			li._treeNode = node.node;
			list.append(li);
		}
		
		return list;
	}
	
	return drawComponentsTreeTraverse(tree);
}


let selected = null;
let dragover = null;

Vvveb.SectionList = {
	
	selector: '.sections-container',
	allowedComponents: {},
	
	init: function(allowedComponents = {}) {

		this.allowedComponents = allowedComponents;

		document.querySelector(this.selector).addEventListener("click", function (e) {
			let element = e.target.closest(":scope > div .controls");
			if (element) {
				let node = element.parentNode._node;
				if (node) {
					node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
					//node.click();
					Vvveb.Builder.selectNode(node);
					Vvveb.Builder.loadNodeComponent(node);
				}
			}
		});
		
		document.querySelector(this.selector).addEventListener("dblclick", function (e) {
			let element = e.target.closest(":scope > div");
			if (element) {
				const node = element._node;
				node.click();
			}
		});
		
		
		document.querySelector(this.selector).addEventListener("click", function (e) {
			let element = e.target.closest("li[data-component] label");
			if (element) {
				let node = element.parentNode._node;
				if (node) {
					node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
					node.click();
				}
			}
		});
		
		document.querySelector(this.selector).addEventListener("mouseenter", function (e) {
			let element = e.target.closest("li[data-component] label");
			if (element) {
				const node = document.querySelector(element.parentNode._node);
				node.css("outline","1px dashed blue");
			}
		});
		
		document.querySelector(this.selector).addEventListener("mouseleave", function (e){
			let element = e.target.closest("li[data-component] label");
			if (element) {
				const node = document.querySelector(element.parentNode._node);
				node.css("outline","");
				if (node.getAttribute("style") == "") node.removeAttribute("style");
			}
		});		
		
		document.querySelector(this.selector).addEventListener("dragstart", this.dragStart);
		document.querySelector(this.selector).addEventListener("dragover", this.dragOver);
		document.querySelector(this.selector).addEventListener("dragend", this.dragEnd);
		
		document.querySelector(this.selector).addEventListener("click", function (e) {
			let element = e.target.closest(".delete-btn");
			if (element) {
				let section = element.closest(".section-item");
				let node = section._node;
				node.remove();
				section.remove();
				Vvveb.TreeList.loadComponents();
				
				e.stopPropagation();
				e.preventDefault();
			}
		});

		let sectionIn;
		let img = document.querySelector(".block-preview img");
		document.querySelector(".sections-list").addEventListener("mouseover", function (e) {
			let element = e.target.closest("li[data-type]");
			if (element) {
				if (sectionIn != element) {
					let src = element.querySelector("img").getAttribute("src");
					sectionIn = element;
					img.setAttribute("src", src);
					img.style.display = "";
				}
			} else {
				sectionIn = element;
				img.setAttribute("src", "");
				img.style.display = "none";
			}
		})
		
		/*
		document.querySelector(this.selector).addEventListener("click", ".up-btn", function (e) {
			let section = e.target.closest(".section-item");
			let node = section._node;
			Vvveb.Builder.moveNodeUp(node);
			Vvveb.Builder.moveNodeUp(section);
			
			e.preventDefault();
		});


		document.querySelector(this.selector).addEventListener("click", ".down-btn", function (e) {
			let section = e.target.closest(".section-item");
			let node = section._node;
			Vvveb.Builder.moveNodeDown(node);
			Vvveb.Builder.moveNodeDown(section);
			
			e.preventDefault();
		});
		*/

		
		let self = this;
		document.querySelector(".sections-list").addEventListener("click", function (e) {
			let element = e.target.closest(".add-section-btn");
			if (element) {
				let item = element.closest("li");
				let section = Vvveb.Sections.get(item.dataset.type);
				let node = generateElements(section.html)[0];
				let sectionType = node.tagName.toLowerCase();
				let afterSection = Vvveb.Builder.frameBody.querySelector(":scope > " + sectionType + ":last-of-type");
				
				if (afterSection) {
					afterSection.after(node);
				} else {
					if (sectionType == "nav") {
						afterSection = Vvveb.Builder.frameBody.querySelector(":scope > nav:first,> header:last-of-type");		
						
						if (afterSection) {
							afterSection.before(node);
						} else {
							Vvveb.Builder.frameBody.append(node);
						}
					} else if (sectionType != "footer") {
						afterSection = Vvveb.Builder.frameBody.querySelector("body > footer:last-of-type");		
						
						if (afterSection) {
							afterSection.before(node);
						} else {
							Vvveb.Builder.frameBody.append(node);
						}
					} else {
						let newsection = Vvveb.Builder.frameBody.querySelector('newsection')
						if (newsection) {
							newsection.before(node);
						} else {
							Vvveb.Builder.frameBody.append(node);
						}
					}
				}
				
				//node.click();

				Vvveb.Server.renderComponents(node);
				Vvveb.Builder.selectNode(node);
				Vvveb.Builder.loadNodeComponent(node);
				/*
				Vvveb.Builder.frameHtml.animate({
					scrollTop: node.offset().top
				}, 1000);
				
				delay(() => node.click(), 1000);
				*/

				Vvveb.Undo.addMutation({type: 'childList', 
										target: node.parentNode, 
										addedNodes: [node], 
										nextSibling: node.nextSibling});								

				
				self.loadSections();
				Vvveb.TreeList.loadComponents();
				Vvveb.TreeList.selectComponent(node);
				node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});

				e.preventDefault();
			}
		});
		
		document.querySelector(this.selector).addEventListener("click", function (e) {
			let element = e.target.closest(".properties-btn");
			if (element) {
				let section = element.closest(".section-item");
				let node = section._node;
				node.click();
				
				e.preventDefault();
			}
		});
		
	},
	
	getSections: function() {
		let sections = [];
		let sectionList = 
			window.FrameDocument.body.querySelectorAll(':scope > section, :scope > header, :scope > footer, :scope > main, :scope > nav');
		
		sectionList.forEach(function (node, i) {
			let id = node.id ? node.id : (node.title ? node.title : node.ariaLabel ?? node.className);
			if (!id) {
				id = 'section-' +  Math.floor(Math.random() * 1000);
			}
			let section = {
				name: id.replace(/[^\w+]+/g,' '),
				id: node.id,
				type: node.tagName.toLowerCase(),
				node: node
			};
			sections.push(section);
		});
		
		return sections;
	},

	loadComponents: function(sectionListItem, section, allowedComponents = {}) {

		let tree = [];
		getNodeTree(section, tree, allowedComponents);
		
		let html = drawComponentsTree(tree);
		document.querySelector("ol", sectionListItem).replaceWith(html);
	},
	
	
	addSection: function(data) {
		let section = generateElements(tmpl("vvveb-section", data))[0];
		section._node = data.node;
		document.querySelector(this.selector).append(section);

		//this.loadComponents(section, data.node, this.allowedComponents);
	},

	loadSections: function() {
		let sections = this.getSections();
		let container = document.querySelector(this.selector);

		container.replaceChildren();
		for (i in sections) {
			this.addSection(sections[i]);
		}

	},
	
	//drag and drop 
	dragOver: function(e) {
		let element = e.target.closest("div");
		if (element) {
			if (e.target != dragover && 
				e.target.className == "section-item") {

				if (dragover) {
					dragover.classList.remove("drag-over");
				}
				
				dragover = e.target;  
				dragover.classList.add("drag-over");
			}
		}
	},

	dragEnd: function (e) {
		let element = e.target.closest("div");
		if (element) {

			if (dragover) {
				let parent = selected.parentNode;
				let selectedNode = selected._node;
				let replaceNode = dragover._node;

				if ((dragover.offsetTop > selected.offsetTop)) {
					//replace section item list
					parent.insertBefore(selected, dragover.nextElementSibling);
					//replace section
					replaceNode.parentNode.insertBefore(selectedNode, replaceNode.nextElementSibling);
				} else {
					//replace section item list
					parent.insertBefore(selected, dragover);
					//replace section
					replaceNode.parentNode.insertBefore(selectedNode, replaceNode);
				}
				
				dragover.classList.remove("drag-over");
				
				let node = selectedNode;
				
				Vvveb.Undo.addMutation({type: 'move', 
									target: node,
									oldParent: node.parentNode,
									oldNextSibling: node.nextSibling});
			}

			selected = null;
			dragover = null;
		}
	},

	dragStart: function (e) {
		let element = e.target.closest("div");
		if (element) {
			selected = e.target
		}
	},
}

Vvveb.StyleList = {
	
	selector: '.sections-container',
	allowedComponents: {},
	
	applyStyle: function(style) {
		let doc =  window.FrameDocument;
		let themeStyle = doc.getElementById("vvvebjs-theme-style");
		
		if (!style) {
			if (themeStyle) themeStyle.remove();
			return;
		}
		
		//if style element does not exist create it			
		if (!themeStyle) {
			themeStyle    = doc.createElement("link");
			themeStyle.id = "vvvebjs-theme-style";
			themeStyle.rel = "stylesheet";
			themeStyle.media = "screen";
			doc.head.append(themeStyle);
		}
		
		themeStyle.href = style;
	},
	
	load: function(doc) {
		let themeStyle = window.FrameDocument.getElementById("vvvebjs-theme-style");
		if (themeStyle) {
			let style = Vvveb.Styles.getByStyle(themeStyle.getAttribute("href"));
			if (style) {
				document.querySelector('.styles-list [data-type="' + style + '"]')?.classList.add("active");
			}
		}
	},
	
	init: function() {

		let self = this;
		document.querySelector(".styles-list").addEventListener("click", function (e) {
			let element = e.target.closest("li");
			if (element) {
				let style = Vvveb.Styles.get(element.dataset.type);
				
				if (element.classList.contains("active")) {
					element.classList.remove("active");
					style = "";//if already active remove current style
				} else {
					document.querySelector(".styles-list .active")?.classList.remove("active");
					element.classList.add("active");
				}
				
				self.applyStyle(style.style);
				
				console.log(style);

				e.preventDefault();
			}
		});
				
	}
}

Vvveb.TreeList = {
	selector: '#tree-list',

	container: null,
	
	tree: [],
	
	idToNode : {},
	
	init: function() {
		// header move
		this.container = document.querySelector(this.selector);
		let header = this.container.querySelector(".header");
		let isDown = false;
		let offset = [0,0];
		let self = this;

		header.addEventListener('mousedown', function(e) {
			if (e.which == 1) {//left click
				isDown = true;
				offset = [
					self.container.offsetLeft - e.clientX,
					self.container.offsetTop - e.clientY
				];
			}
		}, true);

		document.addEventListener('mouseup', function() {
			isDown = false;
		}, true);

		document.addEventListener('mousemove', function(event) {
			if (isDown) {
				event.preventDefault();
				let left = Math.max(event.clientX + offset[0], 0);
				let top = Math.max(event.clientY + offset[1], 0);

				if (left >= 0 && (left < (window.innerWidth - self.container.clientWidth))) self.container.style.left = left + "px";
				if (top >= 0 && (top < (window.innerHeight - self.container.clientHeight))) self.container.style.top  = top + "px";
			}
		});

		document.querySelector(this.selector).addEventListener("click", function (e) {
			let element = e.target.closest("li[data-component]");
			if (element) {
				const node = element._treeNode;
				node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
				//node.click();
				Vvveb.Builder.selectNode(node);
				Vvveb.Builder.loadNodeComponent(node);
				
				document.querySelector(self.selector + " .active")?.classList.remove("active");	
				element.querySelector("label").classList.add("active");
			}
		})
		
		document.querySelector(this.selector).addEventListener("mousemove", function (e) {
			let element = e.target.closest("li[data-component]");
			if (element) {
				const node = element._treeNode;
				
				node.dispatchEvent(new MouseEvent("mousemove", {
					bubbles: true,
					cancelable: true,
				}));
			}
		})
	},
	
	selectComponent: function(node) {
		let id;
		for (const i in this.idToNode) {
			if (node == this.idToNode[i]) {
				id = i;
				break;
			}
		}

		if (id) {
			let element = document.getElementById("id" + id);

			this.container.querySelector(".active")?.classList.remove("active");	
			//collapse all 
			let checkboxes = this.container.querySelectorAll("input[type=checkbox]:checked");
			for (let i = 0, len = checkboxes.length; i < len; i++) {
				checkboxes[i].checked = false;
				let label = checkboxes[i].labels[0];
				if (label) {
					label.classList.remove("active");
				}
			}

			//expand parents
			if (element) {
				let parent = element;
				let current = element;
				while (parent = current.closest("li")) {
					current = parent.parentNode; 
					let input = parent.querySelector("input");
					if (input && input.hasAttribute("type") && input.type == "checkbox") {
						input.checked = true;
					}
				}
				
				element.checked = true;
				element.labels[0].classList.add("active");
				element.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
			}
		}
		
		return false;
	},
	
	loadComponents: function() {
		let list = this.container.querySelector(".tree > ol");
		//if navigator not visible don't load
		if (list.offsetParent === null) return;
		
		this.tree     = [];
		this.idToNode = {};
		getNodeTree(window.FrameDocument.body, this.tree, {}, this.idToNode);
		
		let ol = drawComponentsTree(this.tree);
		list.replaceWith(ol);
		//list.replaceWith(html);
	},	
}

Vvveb.FileManager = {
	tree:false,
	pages:{},
	currentPage: false,
	allowedComponents: {},
	
	init: function(allowedComponents = {}) {
		
		this.allowedComponents = allowedComponents;
		this.tree = document.querySelector("#filemanager .tree > ol");
		this.tree.replaceChildren();
		
		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest("a");
			if (element) {
				e.stopImmediatePropagation();
				if (element.classList.contains('view')) return;
				e.preventDefault();
				return false;
			}
		});	
		
		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest(".delete");
			if (element) {
				Vvveb.FileManager.deletePage(element.closest("li"), e);
				e.stopImmediatePropagation();
				e.preventDefault();
				return false;
			}
		});

		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest(".rename");
			if (element) {
				Vvveb.FileManager.renamePage(element.closest("li"), e, false);
				e.stopImmediatePropagation();
				e.preventDefault();
				return false;
			}
		});

		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest(".duplicate");
			if (element) {
				Vvveb.FileManager.renamePage(element.closest("li"), e, true);
				e.stopImmediatePropagation();
				e.preventDefault();
				return false;
			}
		});
		
		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest("li[data-page] label");
			if (element) {
				let page = element.parentNode.dataset.page;
				if (page) {
					Vvveb.FileManager.loadPage(page, allowedComponents);
					
					let data = Vvveb.FileManager.pages[page];
					
					let pageUrl = window.location.pathname + "?module=editor/editor&template=" + data["file"] + "&url=" + data["url"] + "&name=" + data["title"];
					if (data["post_id"]) {
						pageUrl += "&post_id=" + data["post_id"];
					}
					if (data["product_id"]) {
						pageUrl += "&product_id=" + data["product_id"];
					}
					
					window.history.pushState({name:page}, null, pageUrl); 
				}
				
				e.preventDefault();
				return false;			
			}
		});
		
		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest("li[data-component] label");
			if (element) {
				const node = e.currentTarget.parentNode._node;
				node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
				node.click();
			}
		});
		
		this.tree.addEventListener("mouseenter", function (e) {
			let element = event.target.closest("li[data-component] label");
			if (element) {
				const node = e.currentTarget.parentNode._node;
				node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});

				node.dispatchEvent(new MouseEvent("mousemove", {
					bubbles: true,
					cancelable: true,
				}));
				//node.trigger("mousemove");
			}			
		});
	},
	
	clear: function() {
		this.pages = {};
		this.currentPage = false;
		this.tree.replaceChildren();
	},
	
	deletePage: function(element, e) {
		let page    = element.dataset;
		let post_id = element.dataset.post_id ?? 0;
		let name;
		let _self = this;

		if (post_id) {
			name = element.querySelector('label span')?.textContent;
		} else {
			name = page.file;
		}

		if (confirm(`Are you sure you want to delete "${name}"?`)) {

			//allow event to change page or cancel by setting page to false
			window.dispatchEvent(new CustomEvent("vvveb.FileManager.deletePage", {
				detail: page
			}));
			
			if (page) {
				
				fetch(deleteFileUrl, {method: "POST",  body: new URLSearchParams({file:page.file, post_id, csrf: document.getElementById('csrf')?.value})})
				.then((response) => {
					if (!response.ok) {  return Promise.reject(response);  }
					return response.json()
				})
				.then((data) => {
						let bg = "success";
						if (data.success) {		
							document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
						} else {
							bg = "danger";
						}

						displayToast(bg, "Delete", data.message ?? data);
				})
				.catch(error => {
					let message = error.statusText ?? "Error deleting page!";
					displayToast("danger", "Error", message);

					error.text().then( errorMessage => {
						let message = errorMessage.substr(0, 200);
						displayToast("danger", "Error", message);
					})					
				});

				element.remove();
			}
		}
	},	
	
	renamePage: function(element, e, duplicate = false) {
		let page       = element.dataset;
		let post_id    = element.dataset.post_id ?? 0;
		let product_id = element.dataset.product_id ?? 0;
		let newfile;
		let name;
		let _self = this;

		name = element.querySelector('label span')?.textContent.replace('.html', '');
		if (post_id || product_id) {
			name = prompt(`Enter new name for "${name}"`, name);
			if (name) {
				newfile = page.file;
			}
		} else {
			name = newfile = prompt(`Enter new file name for "${page.file}"`, name.toLowerCase());
		}

		if (newfile && name) {
			//allow event to change page or newfile or cancel by setting page to false
			window.dispatchEvent(new CustomEvent("vvveb.FileManager.renamePage", {
				detail: {page, newfile}
			}));
			
			if (page) {

				fetch(renameFileUrl, {method: "POST",  body: new URLSearchParams({file:page.file, newfile:newfile, name, duplicate, post_id, product_id, csrf: document.getElementById('csrf')?.value})})
				.then((response) => {
					if (!response.ok) {  return Promise.reject(response);  }
					return response.json()
				})
				.then((data) => {
						let bg = "success";
						if (data.success) {		
							//document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
						} else {
							bg = "danger";
						}
						
						newfile = data.newfile ?? newfile;	
						displayToast(bg, "Rename", data.message ?? data);
						let baseName = newfile.replace('.html', '');
						let newName = data.name ?? name ?? friendlyName(newfile.replace(/.*[\/\\]+/, '')).replace('.html', '');
						
						if (duplicate) {
							let addPage = _self.pages[page.page];
							addPage["name"]    = data.name ?? baseName;
							addPage["file"]    = newfile;
							addPage["title"]   = newName;
							addPage["url"]     = data.url;
							addPage["post_id"] = data.post_id;
							addPage["product_id"] = data.product_id;
							
							if (data.newfile) {
								//addPage["url"] = Vvveb.themeBaseUrl + addPage["url"]
								//addPage["url"] =  page.url.substring(0, page.url.lastIndexOf("/") + 1) + addPage["url"];
							}

							let newPage = Vvveb.FileManager.addPage(baseName, addPage, page.page);
							Vvveb.FileManager.scrollToPage(newPage);
						} else {
							_self.pages[page.page]["file"] = newfile;
							_self.pages[page.page]["title"] = newName;
							page.url = data.url ?? page.url.replace(page.file, newfile);
							page.file = newfile;
							element.querySelector(":scope > label span").innerHTML = newName;
							element.querySelector(":scope > label a.view").setAttribute("href", page.url);
							_self.pages[page.page]["url"] = page.url;
							_self.pages[page.page]["file"] = page.file;
						}
				})
				.catch(error => {
					let message = error.statusText ?? "Error renaming page!";
					displayToast("danger", "Error", message);

					error.text().then( errorMessage => {
						let message = errorMessage.substr(0, 200);
						displayToast("danger", "Error", message);
					})
				});				
			}
		}
	},
	
	addPage: function(name, data, afterPage = false, append = true) {

		//allow event to change name or cancel by setting name to false
		window.dispatchEvent(new CustomEvent("vvveb.FileManager.addPage", {
			detail: [name, data],
		}));

		if (!name) {
			return false;
		}

		this.pages[name] = data;
		data['name'] = name;

		let folder = this.tree;
		if (data.folder) {
			if ((data.folder && data.folder != "/") && !(folder = folder.querySelector('li[data-folder="' + data.folder + '"]')))  {
				data.folderTitle = friendlyName(data.folder);//data.folder[0].toUpperCase() + data.folder.slice(1);
				folder = generateElements(tmpl("vvveb-filemanager-folder", data))[0];
				this.tree.append(folder);
			}

			folder = folder.querySelector("ol");
		} 
		
		let page = generateElements(tmpl("vvveb-filemanager-page", data))[0];
		if (afterPage && (afterPage = folder.querySelector('[data-page="' + afterPage + '"]'))) {
			afterPage.after(page);
		} else {
			if (append) {
				folder.append(page);
			} else {
				folder.prepend(page);
			}
		}
		
		return page;
	},
	
	addPages: function(pages) {
		for (page in pages) {
			this.addPage(pages[page]['name'], pages[page]);
		}
	},
	
	addComponent: function(name, url, title, page) {
		document.querySelector("[data-page='" + page + "'] > ol", this.tree).append(
			tmpl("vvveb-filemanager-component", {name:name, url:url, title:title}));
	},
	
	loadComponents: function(allowedComponents = {}) {

		let tree = [];
		getNodeTree(window.FrameDocument.body, tree, allowedComponents);
		
		let html = drawComponentsTree(tree);
		document.querySelector("[data-page='" + this.currentPage + "'] > ol", this.tree).replaceWith(html);
	},
	
	getCurrentUrl: function() {
		if (this.currentPage) {
			return this.pages[this.currentPage]['url'];
		}
	},	
    
	getCurrentPage: function() {
		return this.currentPage;
	},	
    
	getPageData: function(key) {
		if (this.currentPage) {
			if (key) {
				return this.pages[this.currentPage][key];
			} else {
				return this.pages[this.currentPage];
			}
		}
	},	
    
    
	getCurrentFileName: function() {
	    if (this.currentPage) {
		    let folder = this.pages[this.currentPage]['folder'];
		    folder = folder ? folder + '/': ''; 
		    return folder + this.pages[this.currentPage]['file'];
            }
	},
	
	reloadCurrentPage: function() {
		if (this.currentPage)
		return this.loadPage(this.currentPage);
	},
	
	loadPage: function(name, allowedComponents = false, disableCache = true, loadComponents = false) {
		let url = this.pages[name]['url'] ?? "";
		
		if (!url) {
			return;
		}
		
		let page = this.tree.querySelector("[data-page='" + name + "']");
		//remove active from current active page
		this.tree.querySelector("[data-page].active")?.classList.remove("active");
		//set loaded page as active
		page.classList.add("active");
		//open parent folder if closed
		page.closest("[data-folder]")?.querySelector("input[type=checkbox]").setAttribute("checked", true);
		
		this.currentPage = name;
		document.querySelector(".btn-preview-url").setAttribute("href", url);

		//allow event to change page or url or cancel by setting url to false
		let self = this;

		window.dispatchEvent(new CustomEvent("vvveb.FileManager.loadPage", {
			detail: self.pages[name],
		}));

		if (url) {
			Vvveb.Builder.loadUrl(url + (disableCache ? (url.indexOf('?') > -1 ? '&r=':'?r=') + Math.random():''), 
				function () { 
					if (loadComponents) { Vvveb.FileManager.loadComponents(allowedComponents); }
					Vvveb.SectionList.loadSections(allowedComponents); 
					Vvveb.TreeList.loadComponents(); 
					Vvveb.StyleManager.init(); 
					Vvveb.ChangeManager.setOriginalContent();
				});
		}
	},

	scrollToPage: function(page) {
		page.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
	},
}

Vvveb.Breadcrumb = {
	tree:false,	
	
	init: function() {
		this.tree = document.querySelector(".breadcrumb-navigator > .breadcrumb");
		this.tree.replaceChildren();

		this.tree.addEventListener("click", function (e) {
			let element = event.target.closest(".breadcrumb-item");
			if (element) {
				let node = element._node;
				if (node) {
					//node.click();
					Vvveb.Builder.selectNode(node);
					Vvveb.Builder.loadNodeComponent(node);
					node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
				}
				
				e.preventDefault();
			}
		});
		
		let currentHoverNode;
		this.tree.addEventListener("mousemove", function (e) {
			if (event.target == currentHoverNode) return;
			currentHoverNode = event.target;
			
			let element = event.target.closest(".breadcrumb-item");
			if (element) {
				let node = element._node;

				node.dispatchEvent(new MouseEvent("mousemove", {
					bubbles: true,
					cancelable: true,
				}));
			}
		})
	},
	
	addElement: function(data, element) {
		let li = generateElements(tmpl("vvveb-breadcrumb-navigaton-item", data))[0];
		li._node = element;			
		this.tree.prepend(li);
	},
		
	loadBreadcrumb: function(element) {
		this.tree.replaceChildren();
		let currentElement = element;
		
		while (currentElement.parentElement) {
			let elementType = Vvveb.Builder._getElementType(currentElement);
			let el = elementType[1].toLowerCase();
		
			this.addElement({
				"name":  el + " " + elementType[0],
				"className": "el-" + el
			}, currentElement);
			
			currentElement = currentElement.parentElement;
		}
	}
}


Vvveb.Revisions = {

	updateRevisionsDropdown: function (revisions) {
		let dropdown = document.querySelector(".revisions-dropdown");
		let count = Object.keys(revisions).length;
		document.querySelector(".revisions-count").innerHTML = count;
		dropdown.replaceChildren();

		for (let i in revisions) {
			let revision = revisions[i];
			//revision['url'] =  '' + revision['file'] + ".html";
			dropdown.append(generateElements(tmpl('vvveb-revision-item', revision))[0]);
		}
	},
	
	loadRevisions: function (template) {
		let self = this;

		if (template) {
			fetch(revisionsUrl + "&" + new URLSearchParams({template}))
			.then((response) => {
				if (!response.ok) { throw new Error(response) }
				return response.json()
			})
			.then((data) => {
				self.updateRevisionsDropdown(data);					
			})
			.catch(error => {
				console.log(error.statusText);
				displayToast("danger", "Error", "Error loading revision!");
			});			
		}
	},
	
	init: function () {
		let self = this;
		
		window.addEventListener("vvveb.Builder.saveAjax",  function(event) {
			let data = event.detail;
			self.loadRevisions(data['file'] ?? false);
		});
		
		window.addEventListener("vvveb.FileManager.loadPage",  function(event) {
			let data = event.detail;
			self.loadRevisions(data['file'] ?? false);
		});
		
		document.querySelector(".revisions-dropdown").addEventListener("click", function (event) {
			let element = event.target.closest(".btn-revision-load");
			if (element) {
				let file = element.dataset.file;

				if (file) {
					fetch(revisionLoadUrl, {
						method: "POST",  
						headers: {
						  'Content-Type': 'application/x-www-form-urlencoded',
						},
						body: new URLSearchParams({file, csrf: document.getElementById('csrf')?.value})
					})
					.then((response) => {
						if (!response.ok) { throw new Error(response) }
						return response.text()
					})
					.then((data) => {
						Vvveb.Builder.setHtml(data);
						displayToast("success", "Load", "Revision loaded");
						
						Vvveb.Builder.frameBody.querySelectorAll("[data-aos]").forEach(e => e.classList.add("aos-init","aos-animate"));
						document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled"));

						Vvveb.SectionList.loadSections(); 
						Vvveb.TreeList.loadComponents(); 
						Vvveb.StyleManager.init(); 
						Vvveb.ChangeManager.setOriginalContent();
					})
					.catch(error => {
						console.log(error.statusText);
						displayToast("danger", "Error", "Error loading revisions!");
					});					
				}			
			} else {			
				let element = event.target.closest(".btn-revision-delete");
				if (element && confirm("Are you sure?")) {

					let item = element.closest(".dropdown-item");
					let file = element.dataset.file;

					if (file) {
						fetch(revisionDeleteUrl, {
							method: "POST",  
							headers: {
							  'Content-Type': 'application/x-www-form-urlencoded',
							},
							body: new URLSearchParams({file, csrf: document.getElementById('csrf')?.value})
						})
						.then((response) => {
							if (!response.ok) { throw new Error(response) }
							return response.json()
						})
						.then((data) => {
								let bg = "success";
								if (data.success) {	
									item.remove();
								} else {
									bg = "danger";
								}

								displayToast(bg, "Delete", data.message ?? data);
								
								let count = document.querySelector(".revisions-count");
								count.innerHTML = Math.max(0, parseInt(count.innerHTML) - 1);
						})
						.catch(error => {
							console.log(error.statusText);
							displayToast("danger", "Error", "Error deleting revision!");
						});			
					}
				}
			}
		});
	}
}

Vvveb.Server = {
	url: "",
	component: "",
	index: 0,
	fullUpdate: false,
	userServerTemplate: false,
	
	ajax: function (callback) {
		let self = this;
		this.url = Vvveb.Builder.iframe.contentWindow.location.href;

		self.element.style.opacity = "0.85";
		self.element.style.transition = "min-height 0.25s ease-out";
		self.element.style.interpolateSize = "allow-keywords";

		let data = new FormData();
		data.append("_component_content", this.content);
		data.append("html", Vvveb.Builder.getHtml());
		data.append("csrf", document.getElementById('csrf')?.value);
		data.append("user_key", userKey);
		data.append("admin_id", adminId);
		
		fetch(this.url + '&_component_ajax=' + this.component + '&_component_id=' + this.index + '&_server_template=' + this.userServerTemplate + '&r=true',{
			method: 'POST', 
			body: data,
			credentials: 'include'
		})
		.then((response) => {
			if (!response.ok) { throw new Error(response) }
			return response.text()
		})
		.then((data) => {
			if (data) {
			
				let newElement = generateElements(data)[0];
				//set fixed height for parent to avoid page flicker
				let parent = self.element.parentNode;
				
				parent.style.minHeight = parent.clientHeight + "px";
				//full update
				if (this.fullUpdate)  {
					self.element.replaceWith(newElement);
				} else {
					self.element.innerHTML = newElement.innerHTML;
					self.element.style.minHeight = self.element.clientHeight + "px";
				}
				
				Vvveb.Builder.selectNode(self.element);
				
				setTimeout(function () {
					//if (this.fullUpdate) 
					if (parent) parent.style.minHeight = "";
					self.element.style.minHeight = "";
					//self.element.click();
					Vvveb.Builder.selectNode(self.element);
					Vvveb.TreeList.loadComponents();
				}, 500);
			}

			self.element.removeAttribute("style");
			
			if (callback) callback(data);
		})
		.catch(error => {
			console.log(error.statusText);
		});	
	},
	
	//render a single component
	renderComponent:function (element, component, index)  {
		/*
		if (component) {
			this.component = component;
			this.index     = index;
		} else {
			this.component = element.attributes[0].name.replace("data-v-component-", "");
			let selector = "[" + element.attributes[0] + "]";
			this.index =  Array.prototype.indexOf.call(Vvveb.Builder.frameBody.querySelectorAll(selector), element);
		}*/

		 for (attribute of element.attributes) {
			  if (attribute.name.startsWith("data-v-component-")) {
				  this.component = attribute.name.replace("data-v-component-","");
				  break;
			  }
		  }
		
		if (this.content != element.outerHTML) {
			let itemClone = element.cloneNode(true);
			itemClone.querySelectorAll(".vvveb-hidden").forEach(e => e.classList.remove("vvveb-hidden"));
			
			this.content = itemClone.outerHTML;
			this.element = element;
			
			let self = this;
			this.throttle = setTimeout(function () {
				clearTimeout(this.throttle);
				self.ajax();
			}, 500);
		}
		
		return element;
	},
	
	//search for components inside element and render them all
	renderComponents(element) {
		const path = ".//*[ @*[starts-with(name(), 'data-v-component-')] ]";
		const elements = window.FrameDocument.evaluate(path, element ?? window.FrameDocument, null, XPathResult.ANY_TYPE, null);
  
		components = [];
		let currentComponent = elements.iterateNext();
		while (currentComponent) {
		  components.push(currentComponent);
		  currentComponent = elements.iterateNext();
		}  

		for (const component of components) {
		 this.renderComponent(component);
		}
		
		return components;
	}
}

Vvveb.FontsManager = {
	
	activeFonts:[],
	providers: {},//{"google":GoogleFontsManager};

	addFontList: function(provider, groupName, fontList) {
		let fonts = {};
		let fontNames = [];
		
		let fontSelect = generateElements("<optgroup label='" + groupName + "'></optgroup>")[0];
		for (const font in fontList) {
			fontNames.push({"text":font, "value":font, "data-provider": provider});
			let option = new Option(font, font);
			option.dataset.provider = provider;
			//option.style.setProperty("font-family", font);//font preview if the fonts are loaded in editor
			fontSelect.append(option);
		}
		document.getElementById("font-family").append(fontSelect);	

		let list = Vvveb.Components.getProperty("_base", "font-family");
		if (list) {
			list.onChange = function (node,value, input, component) {
				let option = input.options[input.selectedIndex];
				Vvveb.FontsManager.addFont(option.dataset.provider, value, node);
				return node;
			};
			
			list.data.options.push({optgroup:groupName});
			list.data.options = list.data.options.concat(fontNames);

			Vvveb.Components.updateProperty("_base", "font-family", {data:list.data});
			
			//update default font list
			fontList = list.data.options;
		}
	},
	
	addProvider: function(provider, Obj) {
		this.providers[provider] = Obj;
	},
	
	//add also element so we can keep track of the used fonts to remove unused ones
	addFont: function(provider, fontFamily, element = false) {
		if (!provider) return;
		
		let providerObj = this.providers[provider];
		if (providerObj) {
			providerObj.addFont(fontFamily);
			this.activeFonts.push({provider, fontFamily, element});
		}
	},
	
	removeFont: function(provider, fontFamily) {
		if (!provider) return;

		let providerObj = this.providers[provider];
		if (provider!= "default" && providerObj) {
			providerObj.removeFont(fontFamily);
		}
	},
	
	//check if the added fonts are still used for the elements they were set and remove unused ones
	cleanUnusedFonts: function (){
		for (i in this.activeFonts) {
			let elementFont = this.activeFonts[i];
			if (elementFont.element) {
				if (Vvveb.StyleManager.getStyle(elementFont.element,'font-family').replaceAll('"','') != elementFont.fontFamily) {
					this.removeFont(elementFont.provider, elementFont.fontFamily);
				}
			}
		}
	}
};


Vvveb.ColorPalette = {
	colors: {},
	
	getAll: function() {
		return this.colors;
	},
	
	add: function(name, color) {
		this.colors[name] = color;
	},
	
	remove: function(color) {
		delete this.colors[name];
	},
}

function friendlyName(name) {
	name = name.replaceAll("--bs-","").replace(/[-_]/g, " ").trim();  
	return name = name[0].toUpperCase() + name.slice(1);
}

Vvveb.ColorPaletteManager = {
	
	cssVars: {"font": {}, "color" : {}, "dimensions": {}},
	
	getType: function (type) { 
		return this.cssVars[type];
	},
		
	getAllCSSVariableNames:  function (styleSheets = document.styleSheets, selector){

	   for(let i = 0; i < styleSheets.length; i++){
		  try{ 
			 let cssRules =  styleSheets[i].cssRules;
			 for( let j = 0; j < cssRules.length; j++){
				try{
				   let style = cssRules[j].style;	
				   if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue;
				   for(let k = 0; k < style.length; k++){
					  let name = style[k];
					  let value = style.getPropertyValue(name).trim();
					  let type = "";
					  
					  if(name.startsWith("--")){
						//ignore bootstrap rgb variables
						if (name.endsWith("-rgb")) continue;
						//ignore variables depending on other variables
						if (value.startsWith("var(")) continue;

						let friendlyName = name.replace("--bs-","").replaceAll("-", " ");  
						 
						if (value.startsWith("#")) {
							type = "color";
						} else if (value.indexOf('"') >= 0 || value.indexOf("'") >= 0) {
							type = "font";
						} else if (value.endsWith('em') > 0 || value.endsWith('px') > 0) {
							type = "dimensions";
						} else if (!isNaN(parseFloat(value))) {
							type = "dimensions";
						}

						if (type) {
							 if (!this.cssVars[type]) this.cssVars[type] = {};
							 this.cssVars[type][name] = {value, type, friendlyName};
						 }
					  }
				   }
				} catch (error) {}
			 }
		  } catch (error) {}
	   }
	   
	   return this.cssVars;
	},
	
	getCssWithVars:  function (styleSheets = document.styleSheets, vars){
	   let cssVars = {};
	   let css = "";
	   let cssStyles = "";
	   for(let i = 0; i < styleSheets.length; i++){
		  try{ 
			 let cssRules =  styleSheets[i].cssRules;
			 for( let j = 0; j < cssRules.length; j++){
				try{
				   let style = cssRules[j].style;	
				   //if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue;
				   cssStyles = "";
				   for(let k = 0; k < style.length; k++){
					  let name = style[k];
					  let value = style.getPropertyValue(name);
					  if(name.startsWith('--bs-btn-')) {
						  for (v in vars) {
							  if (value == vars[v]) {
								  cssVars[name] = v;
								  cssStyles += name + ":var(" + v + ");\n";
							}
						  }
					  }
					  
				   }
				   if (cssStyles) {
				   css += cssRules[j].selectorText + "{\n"
				   css += cssStyles;
				   css += "}\n";
					}
				} catch (error) {}
			 }
		  } catch (error) {}
	   }
	   return cssVars;
	},

	init: function(document) {
		Vvveb.Components.render("config/bootstrap", "#configuration .component-properties");
		
		//apply current theme color palette
		//let colors = Vvveb.ColorPaletteManager.getType("color");
		let colors = this.cssVars.color;
		for (const name in colors) {
			let color = colors[name].value;

			if (color[0] == "#" && color.length == 7) {//add only valid hex color values 7 char long
				//add color as name to keep values unique
				Vvveb.ColorPalette.add(color, color);
			}
		}
	},
	
};

Vvveb.NewSection = {
	container:null,
	template:`<newsection id="vvvebjs-new-section">
			  <div class="container">
				<div class="row">
				  <div class="col">
					<button id="new-blank-section">Blank section</button>
					<button id="new-section">Add section</button>
				  </div>
				</div>
			  </div>
			</newsection>`,
	
	init: function() {
		this.insert(); 
		
		this.container.onmouseout = function(event) {
		  Vvveb.Builder.highlightEnabled = true;
		};
		
		this.container.onmouseover = function(event) {
		 Vvveb.Builder.highlightEnabled = false;
		};
		
		this.container.onclick = function(event) {
			if (event.target.id == 'new-blank-section') {
				Vvveb.Gui.newSection(null,true);
			} else {
				if (event.target.id == 'new-section') {
					Vvveb.Gui.newSection(null,false);
				}
			}
		};
	},	
	
	insert: function() {
		let position = 'before';
		let lastSection = Vvveb.Builder.frameBody.querySelector(':scope > footer:last-of-type');
		if (!lastSection) {
			position = 'after';
			lastSection = Vvveb.Builder.frameBody.querySelector(':scope > main:last-of-type') || 
			Vvveb.Builder.frameBody.querySelector(':scope > section:last-of-type');
		}
		
		this.container = generateElements(this.template)[0];
		if (lastSection) {
			if (position == 'after') {
				lastSection.after(this.container);
			} else {
				lastSection.before(this.container);
			}
		} else {
			Vvveb.Builder.frameBody.append(this.container)
		}
		
		return this.container;
	}
}

Vvveb.Config = {
	components :[],
	blocks :[],
	plugins :[],
	
	load: function(url = 'default.json') {
		$.getJSON( url, function( data ) {
			
		});
	}
}

// Toggle fullscreen
function launchFullScreen(document) {
  if(document.documentElement.requestFullScreen) {
    
		if (document.FullScreenElement)
			document.exitFullScreen();
		else
			document.documentElement.requestFullScreen();
//mozilla		
  } else if(document.documentElement.mozRequestFullScreen) {

		if (document.mozFullScreenElement)
			document.mozCancelFullScreen();
		else
			document.documentElement.mozRequestFullScreen();
//webkit	  
  } else if(document.documentElement.webkitRequestFullScreen) {

		if (document.webkitFullscreenElement)
			document.webkitExitFullscreen();
		else
			document.documentElement.webkitRequestFullScreen();
//ie	  
  } else if(document.documentElement.msRequestFullscreen) {

		if (document.msFullScreenElement)
			document.msExitFullscreen();
		else
			document.documentElement.msRequestFullscreen();
  }
}

let fontList = [{
	value: "",
	text: "Default"
}, {
	value: "Arial, Helvetica, sans-serif",
	text: "Arial"
}, {
	value: "'Lucida Sans Unicode', 'Lucida Grande', sans-serif",
	text: 'Lucida Grande'
}, {
	value: "'Palatino Linotype', 'Book Antiqua', Palatino, serif",
	text: 'Palatino Linotype'
}, {
	value: "'Times New Roman', Times, serif",
	text: 'Times New Roman'
}, {
	value: "Georgia, serif",
	text: "Georgia, serif"
}, {
	value: "Tahoma, Geneva, sans-serif",
	text: "Tahoma"
}, {
	value: "'Comic Sans MS', cursive, sans-serif",
	text: 'Comic Sans'
}, {
	value: "Verdana, Geneva, sans-serif",
	text: 'Verdana'
}, {
	value: "Impact, Charcoal, sans-serif",
	text: 'Impact'
}, {
	value: "'Arial Black', Gadget, sans-serif",
	text: 'Arial Black'
}, {
	value: "'Trebuchet MS', Helvetica, sans-serif",
	text: 'Trebuchet'
}, {
	value: "'Courier New', Courier, monospace",
	text: 'Courier New'
}, {
	value: "'Brush Script MT', sans-serif",
	text: 'Brush Script'
}];


window.addEventListener("popstate", checkState);

function checkState(e) {
    if (e.state && e.state.name) {
        Vvveb.FileManager.loadPage(e.state.name);
    }
}
