/**
 * 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 engine and buildParams.
 * Based on John Resig's template engine - https://johnresig.com/ - MIT Licensed.
 *
 * @module tmpl
 */
(function () {
	const cache = {};
	const startTag = "{%";
	const endTag = "%}";
	const re1 = new RegExp(`((^|${endTag})[^\t]*)'`, "g");
	const re2 = new RegExp(`\t=(.*?)${endTag}`, "g");

	/**
	 * Compiles a template string into a reusable function and caches it.
	 * Uses `{%` and `%}` as template delimiters.
	 *
	 * @param {string} str - Template string or a DOM element ID containing the template.
	 * @param {Object} [data] - Data object to render into the template.
	 * @returns {Function|string} If data is provided, returns rendered HTML string.
	 *   Otherwise returns the compiled template function for deferred rendering.
	 */
	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.
		const 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;
	};
})();


/**
 * Recursively serializes a nested data structure into URL-encoded parameter segments.
 *
 * @param {string} prefix - Key prefix for serialization (e.g. "data").
 * @param {Array|Object|string|number} obj - Value to serialize.
 * @param {Function} add - Callback invoked with (key, value) for each scalar leaf.
 */
function buildParams(prefix, obj, add) {
	const rbracket = /\[\]$/;

	if (Array.isArray(obj)) {

		// Serialize array item.
		for (const key in obj) {
			const 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);
	}
}

/**
 * Serializes a nested object or array into a URL-encoded query string.
 *
 * @param {Array|Object} a - Form data to serialize.
 * @returns {string} URL-encoded query string (e.g. "key1=val1&key2=val2").
 */
function nestedFormData(a) {
	const s = [],
		add = function (key, valueOrFunction) {

			// If value is a function, invoke it and use its return value
			const 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 a) {
			let v = a[key];
			add(key, v);
		};

	} else {
		for (const prefix in a) {
			buildParams(prefix, a[prefix], add);
		}
	}

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


/**
 * Creates a debounced version of a callback function.
 * The returned function clears any pending timer and sets a new one,
 * ensuring the callback only fires after `ms` milliseconds of inactivity.
 *
 * @param {Function} callback - Function to debounce.
 * @param {number} ms - Milliseconds to wait before invoking the callback.
 */
let delay = (function () {
	let timer = 0;
	return function (callback, ms) {
		clearTimeout(timer);
		timer = setTimeout(callback, ms);
	};
})();

/**
 * Checks whether the given value is a DOM element.
 *
 * @param {*} obj - Value to test.
 * @returns {boolean} True if the object is a DOM element (nodeType 1 with style and ownerDocument).
 */
function isElement(obj) {
	return (typeof obj === "object") &&
		(obj.nodeType === 1) && (typeof obj.style === "object") &&
		(typeof obj.ownerDocument === "object")/* && obj.tagName != "BODY"*/;
}

/**
 * Parses an HTML string into live DOM elements using a <template> element.
 *
 * @param {string} html - HTML string to parse (trimmed before parsing).
 * @returns {HTMLCollection} Collection of child elements parsed from the HTML.
 */
function generateElements(html) {
	const template = document.createElement('template');
	template.innerHTML = html.trim();
	return template.content.children;
}

/**
 * Calculates the absolute page offset of a DOM element.
 *
 * @param {Element} el - DOM element to measure.
 * @returns {{top: number, left: number}} Absolute top and left offset from page origin.
 */
function offset(el) {
	const box = el.getBoundingClientRect();
	const docElem = document.documentElement;
	return {
		top: box.top + window.pageYOffset - docElem.clientTop,
		left: box.left + window.pageXOffset - docElem.clientLeft
	};
}

/**
 * Vvveb builder framework namespace.
 * Provides component, block, section, and style registries plus the builder engine.
 *
 * @namespace Vvveb
 */
if (typeof Vvveb === 'undefined') var Vvveb = {};

/** @type {string} Default component type name used for unmatched elements */
Vvveb.defaultComponent = "_base";

/** @type {boolean} Whether to preserve property sections when rendering component properties */
Vvveb.preservePropertySections = true;

//icon = use component icon when dragging | html = use component html to create draggable element
/** @type {string} Drag display mode: 'icon' uses component icon, 'html' uses component HTML */
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
/** @type {string} Inline CSS style applied to the drag placeholder element */
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;";

/** @type {string} HTML string for the drag placeholder element */
Vvveb.dragHtml = '<div style="' + Vvveb.dragElementStyle + '"></div>';

/** @type {string} Base URL for builder scripts derived from currentScript src */
Vvveb.baseUrl = document.currentScript ? document.currentScript.src.replace(/[^\/]*?\.js$/, '') : '';

/** @type {string} Base URL for image assets (same as baseUrl) */
Vvveb.imgBaseUrl = Vvveb.baseUrl;

/** @type {Object.<string, Array>} Registry mapping category names to component type arrays */
Vvveb.ComponentsGroup = {};

/** @type {Object.<string, Array>} Registry mapping category names to section type arrays */
Vvveb.SectionsGroup = {};

/** @type {Object.<string, Array>} Registry mapping category names to block type arrays */
Vvveb.BlocksGroup = {};

/** @type {Object.<string, Array>} Registry mapping category names to style type arrays */
Vvveb.StylesGroup = {};

/**
 * Component registry. Manages all registered component definitions and their
 * lookup tables for matching DOM nodes to components by tag, attribute, or class.
 *
 * @namespace Components
 * @memberof Vvveb
 */
Vvveb.Components = {

	/** @type {Object.<string, Object>} Map of component type name to component definition */
	_components: {},

	/** @type {Object.<string, Object>} Lookup table: tag name → component definition */
	_nodesLookup: {},

	/** @type {Object.<string, Object>} Lookup table: attribute name → component definition */
	_attributesLookup: {},

	/** @type {Object.<string, Object>} Lookup table: CSS class name → component definition */
	_classesLookup: {},

	/** @type {Object.<RegExp, Object>} Lookup table: CSS class regex → component definition */
	_classesRegexLookup: {},

	/** @type {string} CSS selector for the component properties panel */
	componentPropertiesElement: "#right-panel .component-properties",

	/** @type {string} Default section name for rendering component properties */
	componentPropertiesDefaultSection: "content",

	/**
	 * Retrieves a component definition by type.
	 *
	 * @param {string} type - Component type name.
	 * @returns {Object|undefined} The registered component definition, or undefined.
	 */
	get: function (type) {
		return this._components[type];
	},

	/**
	 * Updates a specific property within a component definition.
	 *
	 * @param {string} type - Component type name.
	 * @param {string} key - Property key to find.
	 * @param {Object} value - New values to merge into the property.
	 * @returns {Object} The updated property object.
	 */
	updateProperty: function (type, key, value) {
		const properties = this._components[type]["properties"];
		for (const property in properties) {
			if (key == properties[property]["key"]) {
				return this._components[type]["properties"][property] =
					Object.assign(properties[property], value);
			}
		}
	},

	/**
	 * Finds and returns a property object by key within a component's properties.
	 *
	 * @param {string} type - Component type name.
	 * @param {string} key - Property key to find.
	 * @returns {Object|undefined} The property object, or undefined if not found.
	 */
	getProperty: function (type, key) {
		const properties = this._components[type] ? this._components[type]["properties"] : [];
		for (const property in properties) {
			if (key == properties[property]["key"]) {
				return properties[property];
			}
		}
	},

	/**
	 * Registers a component definition. Populates lookup tables for nodes,
	 * attributes, classes, and class regex patterns.
	 *
	 * @param {string} type - Component type name.
	 * @param {Object} data - Component definition (nodes, attributes, classes, classesRegex, properties, etc.).
	 */
	add: function (type, data) {
		data.type = type;

		this._components[type] = data;

		if (data.nodes) {
			for (const i in data.nodes) {
				this._nodesLookup[data.nodes[i]] = data;
			}
		}

		if (data.attributes) {
			if (data.attributes.constructor === Array) {
				for (const i in data.attributes) {
					this._attributesLookup[data.attributes[i]] = data;
				}
			} else {
				for (const 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 (const i in data.classes) {
				this._classesLookup[data.classes[i]] = data;
			}
		}

		if (data.classesRegex) {
			for (const i in data.classesRegex) {
				this._classesRegexLookup[data.classesRegex[i]] = data;
			}
		}
	},

	/**
	 * Creates a new component by inheriting from an existing one.
	 * Merges properties from parent and child (child first), sorts by sort key,
	 * then registers the new component.
	 *
	 * @param {string} inheritType - Parent component type name to inherit from.
	 * @param {string} type - New child component type name.
	 * @param {Object} data - Additional or overriding component definition data.
	 */
	extend: function (inheritType, type, data) {

		let newData = data;
		let inheritData = this._components[inheritType];

		if (inheritData) {
			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;

			return a.sort - b.sort;
		});

		this.add(type, newData);
	},


	/**
	 * Matches a DOM node to a registered component definition.
	 * Checks attributes, CSS classes (exact and regex), then tag name.
	 *
	 * @param {Element} node - DOM node to match.
	 * @returns {Object|false} Matching component definition, or false if no match.
	 */
	matchNode: function (node) {
		let component = {};

		if (!node || !node.tagName) return false;

		if (node.attributes && node.attributes.length) {
			//search for attributes
			for (const i in node.attributes) {
				if (node.attributes[i]) {
					const attr = node.attributes[i].name;
					const 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 (const i in node.attributes) {
				const attr = node.attributes[i].name;
				const value = node.attributes[i].value;

				//check for node classes
				if (attr == "class") {
					const classes = value.split(" ");

					for (const j in classes) {
						if (classes[j] in this._classesLookup)
							return this._classesLookup[classes[j]];
					}

					for (const regex in this._classesRegexLookup) {
						const regexObj = new RegExp(regex);
						if (regexObj.exec(value)) {
							return this._classesRegexLookup[regex];
						}
					}
				}
			}
		}

		const tagName = node.tagName.toLowerCase();
		if (tagName in this._nodesLookup) return this._nodesLookup[tagName];

		return false;
	},

	/**
	 * Renders a component's property definitions into the UI panel.
	 * For each property: initializes the input, sets its value, wires up
	 * the propertyChange event listener, and appends to the appropriate section.
	 *
	 * @param {Object} component - Component definition object.
	 * @param {Array} properties - Array of property definitions.
	 * @param {Object} componentsPanelSections - Map of section name to DOM panel elements.
	 * @param {Element} [container] - Specific container to render into (optional).
	 */
	renderProperties: function (component, properties, componentsPanelSections, container) {
		const setupPropertyInput = function (component, property) {
			if (property.input) {
				property.input.addEventListener('propertyChange', (event) => {
					let selectedElement = Vvveb.Builder.selectedEl;
					element = selectedElement;
					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.closest(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, oldStyle;

						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
							let 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") {
						const 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 (const 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);
			}

			setupPropertyInput(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);
			}
		}
	},

	/**
	 * Renders the full property panel for a given component type.
	 * Clears existing properties, creates the default section header,
	 * renders properties via renderProperties, then calls init.
	 *
	 * @param {string} type - Component type name.
	 * @param {string|boolean} [panel=false] - CSS selector for the properties panel, or false for default.
	 */
	render: function (type, panel = false) {

		const 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);
	}
};


/**
 * Block registry. Manages draggable block definitions for the sidebar.
 *
 * @namespace Blocks
 * @memberof Vvveb
 */
Vvveb.Blocks = {

	/** @type {Object.<string, Object>} Map of block type name to block definition */
	_blocks: {},

	/**
	 * Returns a block definition by type.
	 *
	 * @param {string} type - Block type name.
	 * @returns {Object|undefined} The block definition, or undefined.
	 */
	get: function (type) {
		return this._blocks[type];
	},

	/**
	 * Registers a block definition.
	 *
	 * @param {string} type - Block type name.
	 * @param {Object} data - Block definition (name, html, image, etc.).
	 */
	add: function (type, data) {
		data.type = type;
		this._blocks[type] = data;
	},
};

/**
 * Section registry. Manages reusable section definitions for the sidebar.
 *
 * @namespace Sections
 * @memberof Vvveb
 */
Vvveb.Sections = {

	/** @type {Object.<string, Object>} Map of section type name to section definition */
	_sections: {},

	/**
	 * Returns a section definition by type.
	 *
	 * @param {string} type - Section type name.
	 * @returns {Object|undefined} The section definition, or undefined.
	 */
	get: function (type) {
		return this._sections[type];
	},

	/**
	 * Registers a section definition.
	 *
	 * @param {string} type - Section type name.
	 * @param {Object} data - Section definition (name, html, image, etc.).
	 */
	add: function (type, data) {
		data.type = type;
		this._sections[type] = data;
	},
};

/**
 * Style/theme registry. Manages CSS stylesheet definitions for theme switching.
 *
 * @namespace Styles
 * @memberof Vvveb
 */
Vvveb.Styles = {

	/** @type {Object.<string, Object>} Map of style type name to style definition */
	_styles: {},

	/**
	 * Returns a style definition by type.
	 *
	 * @param {string} type - Style type name.
	 * @returns {Object|undefined} The style definition, or undefined.
	 */
	get: function (type) {
		return this._styles[type];
	},

	/**
	 * Looks up a style definition by its stylesheet URL value.
	 *
	 * @param {string} style - Stylesheet URL to search for.
	 * @returns {string|undefined} The type name of the matching style, or undefined.
	 */
	getByStyle: function (style) {
		for (const name in this._styles) {
			if (this._styles[name].style == style) {
				return name;
			}
		}
	},

	/**
	 * Registers a style definition.
	 *
	 * @param {string} type - Style type name.
	 * @param {Object} data - Style definition (name, style href, etc.).
	 */
	add: function (type, data) {
		data.type = type;
		this._styles[type] = data;
	},
};


/**
 * Inline WYSIWYG text editor for the builder iframe.
 * Handles text selection styling, bold/italic/underline, links, colors, and fonts.
 *
 * @namespace WysiwygEditor
 * @memberof Vvveb
 */
Vvveb.WysiwygEditor = {

	/** @type {boolean} Whether the WYSIWYG editor is currently active */
	isActive: false,
	/** @type {string} InnerHTML of the element before editing, used for undo */
	oldValue: '',
	/** @type {Document|boolean} Reference to the editable document; false when not initialized */
	doc: false,


	/**
	 * Wraps the current selection in a tag and applies inline CSS styles.
	 * Supports toggling styles on/off and handles empty span cleanup.
	 *
	 * @param {string|boolean} tag - HTML tag to wrap selection in (e.g. "span", "a"), or false for span.
	 * @param {Object} [style={}] - CSS property/value pairs to apply.
	 * @param {boolean} [toggle=false] - If true, removes style properties that already exist.
	 * @returns {Element} The element containing the styled selection.
	 */
	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 (const 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;
	},

	/**
	 * Initializes the WYSIWYG editor by attaching event listeners to toolbar buttons
	 * (bold, italic, underline, strikethrough, link, colors, font-size, font-family, alignment).
	 *
	 * @param {Document} doc - The iframe document.
	 */
	init: function (doc) {
		this.doc = doc;
		const 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 (let 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) {
			const 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.target.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();
				}
			}
		})
	},

	/**
	 * Executes the browser's undo command on the iframe document.
	 *
	 * @param {Element} element - Unused.
	 */
	undo: function (element) {
		this.doc.execCommand('undo', false, null);
	},

	/**
	 * Executes the browser's redo command on the iframe document.
	 *
	 * @param {Element} element - Unused.
	 */
	redo: function (element) {
		this.doc.execCommand('redo', false, null);
	},

	/**
	 * Activates WYSIWYG editing on a given element.
	 * Sets contenteditable, shows toolbar, stores original HTML for undo.
	 *
	 * @param {Element} element - DOM element to make editable.
	 */
	edit: function (element) {
		element.setAttribute("contenteditable", true);
		element.setAttribute("spellcheck", 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();
	},

	/**
	 * Deactivates WYSIWYG editing and records an undo mutation.
	 *
	 * @param {Element} element - The element being edited.
	 */
	destroy: function (element) {
		element.removeAttribute("contenteditable");
		element.removeAttribute("spellcheck");

		document.getElementById("wysiwyg-editor").style.display = "none";
		this.isActive = false;


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

/**
 * Core builder engine. Manages the iframe canvas, element selection,
 * drag-and-drop, node manipulation, saving, and HTML generation.
 *
 * @namespace Builder
 * @memberof Vvveb
 */
Vvveb.Builder = {

	/** @type {Object} The currently selected component definition */
	component: {},
	/** @type {HTMLIFrameElement|null} Reference to the builder iframe element */
	iframe: null,
	/** @type {Object|false} Pending undo mutation for a drag-move operation */
	dragMoveMutation: false,
	/** @type {boolean} Whether the builder is in preview mode */
	isPreview: false,
	/** @type {boolean} Whether to execute JS when setting HTML content */
	runJsOnSetHtml: false,
	/** @type {boolean} Whether designer mode (absolute positioning) is enabled */
	designerMode: false,
	/** @type {boolean} Whether element highlighting on hover is active */
	highlightEnabled: false,
	/** @type {number} Padding in pixels around the selection box */
	selectPadding: 0,
	/** @type {number} Width of the left panel in pixels */
	leftPanelWidth: 275,
	/** @type {Array<string>} CSS class names to ignore in selectors */
	ignoreClasses: ["clearfix", "masonry", "has-shadow"],

	/**
	 * Initializes the builder. Loads control/section/block/style groups,
	 * sets up the iframe, drag-and-drop, and selection box.
	 *
	 * @param {string} url - URL to load in the iframe.
	 * @param {Function} [callback] - Callback function after iframe load.
	 */
	init: function (url, callback) {

		const 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");

		if (!url) {
			url = 'about:blank';
		}

		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 */
	/**
	 * Populates .components-list elements with grouped component items.
	 */
	loadControlGroups: function () {

		const componentsList = document.querySelectorAll(".components-list");
		let count = 0;

		componentsList.forEach(function (list, i) {
			let type = list.dataset.type;
			list.replaceChildren();
			count++;

			for (const 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');

				const components = Vvveb.ComponentsGroup[group];

				for (const 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);
					}
				}
			}
		});
	},

	/**
	 * Populates .sections-list elements with grouped section items.
	 */
	loadSectionGroups: function () {

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

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

			for (const 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 (const 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)
					}
				}
			}
		});
	},

	/**
	 * Populates .blocks-list elements with grouped block items.
	 */
	loadBlockGroups: function () {

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

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

			for (const 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');
				const blocks = Vvveb.BlocksGroup[group];

				for (const 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);
					}
				}
			}
		});
	},

	/**
	 * Populates .styles-list elements with grouped style items.
	 */
	loadStylesGroups: function () {

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

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

			for (const 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');
				const styles = Vvveb.StylesGroup[group];

				for (const 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);
					}
				}
			}
		});
	},

	/**
	 * Loads a new URL into the builder iframe.
	 *
	 * @param {string} url - New URL to load.
	 * @param {Function} [callback] - Callback after load.
	 */
	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 */
	/**
	 * Loads a URL into the iframe and attaches the load event handler.
	 *
	 * @param {string} url - URL to load.
	 */
	_loadIframe: function (url) {

		const 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;
				}
			});

			const 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();
		});

	},

	/**
	 * Called after the iframe is fully loaded. Sets up frameDoc/frameHtml/frameBody,
	 * injects editor CSS, initializes highlighting and other managers.
	 */
	_frameLoaded: function () {

		const 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);
	},

	/**
	 * Determines the display identifier for a DOM element.
	 *
	 * @param {Element} el - DOM element.
	 * @returns {Array<string>} [componentName, tagName] tuple.
	 */
	_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];
	},

	/**
	 * Matches a node to a component type, loads its properties panel.
	 *
	 * @param {Element} node - DOM node to inspect.
	 */
	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();
		}
	},

	/**
	 * Re-renders the properties panel for the currently selected component.
	 */
	reloadComponent: function () {
		Vvveb.Components.render(this.selectedComponent);
	},

	/**
	 * Moves a DOM node up in the document tree.
	 *
	 * @param {Element} [node] - Node to move; defaults to selectedEl.
	 */
	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
		});

	},

	/**
	 * Moves a DOM node down in the document tree.
	 *
	 * @param {Element} [node] - Node to move; defaults to selectedEl.
	 */
	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
		});
	},

	/**
	 * Deep-clones a node and inserts the clone after the original.
	 *
	 * @param {Element} [node] - Node to clone; defaults to selectedEl.
	 */
	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
		});

	},


	/**
	 * Selects a DOM node: positions the selection box, loads breadcrumb,
	 * updates highlight, and dispatches a selectNode event.
	 *
	 * @param {Element|false|null} node - Node to select, or falsy to deselect.
	 * @returns {boolean|undefined} Returns false on error.
	 */
	selectNode: function (node) {
		const SelectBox = document.getElementById("select-box");

		if (!node) {
			SelectBox.style.display = "none";
			return;
		}

		const self = this;
		const 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.error(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 */
	/**
	 * Sets up mouse event listeners on frameBody for element highlighting,
	 * resize dragging, element dragging, double-click text editing, and click selection.
	 */
	_initHighlight: function () {

		const self = Vvveb.Builder;

		let highlightMove = function (event) {
			if (self.highlightEnabled == true && event.target/* && isElement(event.target)*/) {

				let target = event.target;
				self.highlightEl = 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 (const 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.error(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").textContent = elementType[0];
					document.querySelector("#highlight-name .name").textContent = 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));
							const 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);
				Vvveb.Server.renderComponents(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;
					let target = event.target;
					self.texteditEl = target;

					Vvveb.WysiwygEditor.edit(self.texteditEl);

					const _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);

	},

	/**
	 * Initializes all select-box action buttons: drag, resize, move up/down,
	 * clone, parent navigation, save reusable, edit code, translate, delete, add section.
	 */
	_initBox: function () {
		const 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 (let code in data) {
						let translation = data[code];
						document.querySelector("#lang-" + code + "-editor textarea").value = translation;
					}
				})
				.catch(error => {
					console.error(error);
					displayToast("danger", "Error", "Error loading translations!");
				});


/**
 * Translation editor modal. Inherits from ModalCodeEditor.
 *
 * @namespace ModalTranslateEditor
 * @memberof Vvveb
 * @extends ModalCodeEditor
 */
			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) {
						return Promise.resolve(response.text()).then((responseInText) => {
							return Promise.reject([response, responseInText]);
						});
					}
					return response.json();
				})
				.then((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);
					}

				})
				.catch(err => {
					displayToast("danger", "Error", "Error saving translations!");

					let message = err;
					try {
						let [response, responseInText] = err;
						message = responseInText ?? response.statusText ?? "Error uploading!";
					} catch (e) {
					}

					if (typeof err?.text === 'function') err.text().then(errorMessage => {
						let message = errorMessage.substring(0, 200);
						displayToast("danger", "Error", message);
					});

					if (typeof message == "string") {
						displayToast("danger", "Error", message.substring(0, 200));
					}

				});
			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 */
	/**
	 * Initializes the sidebar drag-and-drop system for components, blocks, and sections.
	 */
	_initDragdrop: function () {

		const 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.pageX || event.clientX;
			let y = event.pageY || 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 = "");
			}
		});

	},

	/**
	 * Removes editor helper elements, newsection elements, and optionally data-vvveb-* attributes.
	 *
	 * @param {string} html - HTML to clean.
	 * @param {boolean} [keepHelperAttributes=false] - Whether to keep data-vvveb-* attributes.
	 * @returns {string} Cleaned HTML.
	 */
	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;
	},

	/**
	 * Generates the complete page HTML for saving. Removes contenteditable, cleans
	 * helper elements, and dispatches filter events.
	 *
	 * @param {boolean} [keepHelperAttributes=true] - Keep data-vvveb-* attributes.
	 * @param {boolean} [filter=true] - Whether to dispatch filter events.
	 * @returns {string} Full HTML document string.
	 */
	getHtml: function (keepHelperAttributes = true, filter = true) {
		const doc = window.FrameDocument;
		const hasDoctpe = (doc.doctype !== null);
		let html = "";

		doc.querySelectorAll("[contenteditable]").forEach(e => e.removeAttribute("contenteditable"));
		doc.querySelectorAll("[spellcheck]").forEach(e => e.removeAttribute("spellcheck"));
		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;
	},

	/**
	 * Loads HTML into the iframe using srcdoc.
	 *
	 * @param {string} html - Full HTML document string.
	 */
	loadHtml: function (html) {
		this.iframe.srcdoc = html;
		/*
		this.iframe.src = "about:blank";
		window.FrameDocument.open();
		window.FrameDocument.write(html);
		window.FrameDocument.close();
		*/
	},

	/**
	 * Sets the iframe content by extracting body and head sections from an HTML string.
	 *
	 * @param {string} html - Full HTML document string.
	 */
	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;
		}
	},

	/**
	 * Saves a selected element as a reusable block or section.
	 *
	 * @param {Element} element - DOM element to save.
	 * @param {string} type - "section" or "block".
	 * @param {string} name - Reusable element name.
	 * @param {Function} [callback] - Callback after save.
	 */
	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 || data == "success") {
			} else {
				bg = "danger";
			}

			displayToast(bg, "Save", data.message ?? data);
		})
		.catch(error => {
			console.error(error);
			displayToast("danger", "Error", "Error saving!");
		});
	},

	/**
	 * Saves the current page via POST request.
	 *
	 * @param {Object} [data] - Additional page data.
	 * @param {string} saveUrl - Server endpoint URL.
	 * @param {Function} [success] - Success callback.
	 * @param {Function} [error] - Error callback.
	 * @returns {Promise} The fetch Promise.
	 */
	save: function (data, saveUrl, success, error) {
		if (!data) data = Vvveb.FileManager.getPageData();

		if (!Object.hasOwn(data, "file")) {
			data["file"] = Vvveb.FileManager.getCurrentFileName();
		}

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

		data["elements"] = Vvveb.ChangeManager.getChangedElements();
		data["csrf"] = document.getElementById('csrf')?.value;

		if (!Object.hasOwn(data, "startTemplateUrl")) {
			data["html"] = this.getHtml();
		}

		let detail = { ...Vvveb.FileManager.getPageData(), ...data };
		//allow event to change name or cancel by setting name to false
		window.dispatchEvent(new CustomEvent("Vvveb.Builder.save", {
			detail
		}));
		//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 (success) success(data);
				Vvveb.Undo.reset();
				document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true"));
				window.dispatchEvent(new CustomEvent("vvveb.Builder.save", {
					detail: data,
				}));
			})
			.catch((err) => {
				if (error) error(err);

				let message;

				if (Object.hasOwn(err, 'message')) {
					message = err.message;
				} else if (err?.[Symbol.iterator]) {
					let [response, responseInText] = err;
					message = responseInText || response.statusText || "Error saving!";

					if (typeof response?.text === 'function') response.text().then(errorMessage => {
						let message = errorMessage.substring(0, 200);
						displayToast("danger", "Error", message);
					});
				}

				if (message) {
					displayToast("danger", "Error", message.substring(0, 200));
				}
			});
	},

	/**
	 * Enables or disables designer mode (absolute element positioning).
	 *
	 * @param {boolean} [designerMode=false] - Whether designer mode is enabled.
	 */
	setDesignerMode: function (designerMode = false) {
		this.designerMode = designerMode;
	}

};

/**
 * Modal dialog wrapper for code editing (HTML editing).
 *
 * @namespace ModalCodeEditor
 * @memberof Vvveb
 */
Vvveb.ModalCodeEditor = {
	/** @type {Element|false} The modal DOM element */
	modal: false,
	/** @type {string} DOM ID of the modal */
	modalId: 'codeEditorModal',
	/** @type {string} Event name prefix for custom events */
	name: 'ModalCodeEditor',
	/** @type {Element|false} The textarea element used as the editor */
	editor: false,

	/**
	 * Initializes the modal by finding elements and attaching save button listener.
	 *
	 * @param {Element} [modal] - Modal element override.
	 * @param {Element} [editor] - Editor textarea override.
	 */
	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');
		}

		const 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;
		});
	},

	/**
	 * Shows the Bootstrap modal.
	 *
	 * @param {*} value - Unused.
	 */
	show: function (value) {
		if (!this.modal) {
			this.init();
		}

		const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal);
		return bsModal.show();
	},

	/**
	 * Hides the Bootstrap modal.
	 *
	 * @param {*} value - Unused.
	 */
	hide: function (value) {
		const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal);
		return bsModal.hide();
	},

	/**
	 * Returns the current editor textarea value.
	 *
	 * @returns {string} Current HTML content.
	 */
	getValue: function () {
		return this.editor.value;
	},

	/**
	 * Sets the editor textarea value and enables save buttons.
	 *
	 * @param {string} value - HTML content to set.
	 */
	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 = Object.create(Vvveb.ModalCodeEditor);
	Vvveb.ModalTranslateEditor.modalId = 'translateEditorModal';
	Vvveb.ModalTranslateEditor.name = 'ModalTranslateEditor';

/**
 * Full-page source code editor panel for editing the iframe HTML.
 *
 * @namespace CodeEditor
 * @memberof Vvveb
 */
Vvveb.CodeEditor = {

	/** @type {boolean} Whether the code editor is currently active */
	isActive: false,
	/** @type {string} Previous editor value */
	oldValue: '',
	/** @type {Document|boolean} Document reference */
	doc: false,
	/** @type {Element|false} The textarea element */
	textarea: false,

	/**
	 * Initializes the code editor. Populates textarea from iframe HTML,
	 * attaches debounced keyup listener to push content back to iframe.
	 *
	 * @param {Document} doc - Unused.
	 */
	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;
	},

	/**
	 * Refreshes the textarea with the current iframe HTML.
	 *
	 * @param {*} value - Unused.
	 */
	setValue: function (value) {
		if (this.isActive) {
			this.textarea.value = Vvveb.Builder.getHtml();
		}
	},

	/**
	 * Destroys the code editor (currently a no-op).
	 *
	 * @param {Element} element - Unused.
	 */
	destroy: function (element) {
		//this.isActive = false;
	},

	/**
	 * Toggles the code editor on/off.
	 */
	toggle: function () {
		if (this.isActive != true) {
			this.isActive = true;
			return this.init();
		}
		this.isActive = false;
		this.destroy();
	}
}


/**
 * CSS editor panel for editing the page's custom styles.
 *
 * @namespace CssEditor
 * @memberof Vvveb
 */
Vvveb.CssEditor = {

	/** @type {boolean} Whether the CSS editor is active */
	isActive: false,
	/** @type {string} Previous CSS value */
	oldValue: '',
	/** @type {Document|boolean} Document reference */
	doc: false,
	/** @type {Element|false} The textarea element */
	textarea: false,

	/**
	 * Initializes the CSS editor. Populates textarea from StyleManager.
	 *
	 * @param {Document} doc - Unused.
	 */
	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);
		});
	},

	/**
	 * Returns the current CSS textarea value.
	 *
	 * @returns {string} Current CSS content.
	 */
	getValue: function () {
		return this.textarea.value;
	},

	/**
	 * Sets the textarea value and optionally pushes to StyleManager.
	 *
	 * @param {string} value - CSS text.
	 * @param {boolean} [updateStyles=true] - Whether to push to StyleManager.
	 */
	setValue: function (value, updateStyles = true) {
		this.textarea.value = value;
		if (updateStyles) {
			Vvveb.StyleManager.setCss(value);
		}
	},

	/**
	 * No-op.
	 */
	destroy: function () {
	},

	/**
	 * Toggles the CSS editor on/off.
	 */
	toggle: function () {
		if (this.isActive != true) {
			this.isActive = true;
			return this.init();
		}
		this.isActive = false;
		this.destroy();
	}
}

/**
 * Displays a Bootstrap toast notification.
 *
 * @param {string} type - Toast type ("danger", "success", etc.).
 * @param {string} title - Toast header title.
 * @param {string} message - Toast body message.
 * @param {string} [position='bottom'] - "bottom" or "top".
 * @param {string} [id] - Optional toast element ID.
 */
function displayToast(type, title, message, position = 'bottom', id = null) {
	if (!id) {
		id = position + "-toast";
	}

	const toast = document.getElementById(id);
	const header = toast.querySelector(".toast-header");
	toast.classList.remove("bottom-0", "top-0");
	toast.classList.add(position + "-0");
	toast.querySelector(".toast-body .message").textContent = message;
	header.classList.remove("danger", "success");
	header.classList.add("bg-" + type);
	header.querySelector("strong").textContent = title;
	const 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();
}

/**
 * GUI controller. Wires UI buttons and actions to builder functions.
 *
 * @namespace Gui
 * @memberof Vvveb
 */
Vvveb.Gui = {

	/**
	 * Binds all elements with data-vvveb-action attributes to their
	 * corresponding Vvveb.Gui methods.
	 */
	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();
	},

	/**
	 * Registers keyboard shortcuts: Ctrl+S (save), Ctrl+Z (undo),
	 * Ctrl+Y (redo), Ctrl+L (tree list), Ctrl+E (editor), Ctrl+P (new page).
	 */
	shortcuts: function () {
		let self = this;

		const 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.save(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);
		});

	},

	/**
	 * Executes undo in the WYSIWYG editor or Vvveb.Undo.
	 */
	undo: function () {
		if (Vvveb.WysiwygEditor.isActive) {
			Vvveb.WysiwygEditor.undo();
		} else {
			Vvveb.Undo.undo();
		}
		Vvveb.Builder.selectNode();
	},

	/**
	 * Executes redo in the WYSIWYG editor or Vvveb.Undo.
	 */
	redo: function () {
		if (Vvveb.WysiwygEditor.isActive) {
			Vvveb.WysiwygEditor.redo();
		} else {
			Vvveb.Undo.redo();
		}
		Vvveb.Builder.selectNode();
	},

	//show modal with html content
	/**
	 * Shows a modal with the current HTML in a textarea.
	 */
	saveModal: function () {
		document.getElementById('textarea-modal textarea').value = Vvveb.Builder.getHtml();
		document.getElementById('textarea-modal').modal();
	},

	//show offcanvas with page changes
	/**
	 * Opens an offcanvas panel showing page change information.
	 */
	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
	/**
	 * Initiates a save operation with loading state and toast notifications.
	 *
	 * @param {Event} event - Click event.
	 * @param {string} [saveUrl] - Save URL override.
	 * @param {Element} [saveBtn] - The save button element.
	 * @returns {Promise} The save Promise.
	 */
	save: 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");

		let detail = { ...pageData, saveUrl, saveBtn };
		const saveEvent = new CustomEvent("Vvveb.Gui.save", { detail });
		window.dispatchEvent(saveEvent);

		return Vvveb.Builder.save(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);
		});
	},

	/**
	 * Downloads the current page HTML as a file.
	 */
	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;
		}
	},

	/**
	 * Switches the iframe viewport width to a responsive breakpoint.
	 */
	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");
		}
		
		Vvveb.StyleManager.setMedia();
	},

	/**
	 * Toggles the bottom code/CSS editor panel.
	 */
	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();
	},

	/**
	 * Toggles visibility of hidden elements in the iframe.
	 */
	toggleHidden: function () {
		Vvveb.Builder.frameBody.classList.toggle("vvveb-hidden");
	},

	/**
	 * Toggles whether JavaScript executes when setting HTML content.
	 */
	toggleEditorJsExecute: function () {
		Vvveb.Builder.runJsOnSetHtml = this.checked;
	},

	/**
	 * Toggles preview mode (hides iframe overlays).
	 */
	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");
	},

	/**
	 * Enters fullscreen mode.
	 */
	fullscreen: function () {
		launchFullScreen(document); // the whole page
	},

	/**
	 * Filters components/blocks/sections list by search text.
	 */
	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 = "";
		});
	},

	/**
	 * Clears the search input and resets the filter.
	 *
	 * @param {Event} e - Event.
	 */
	clearSearch: function (e) {
		let input = this.parentNode.querySelector("input");
		input.value = "";
		input.dispatchEvent(new KeyboardEvent("keyup", {
			bubbles: true,
			cancelable: true,
		}));
	},

	/**
	 * Toggles all section groups open/closed.
	 *
	 * @param {Event} e - Event.
	 */
	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 
	/**
	 * Opens the new page modal and handles creation on submit.
	 */
	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['file'] = data['title'] + '.html';
			}

			if (!data['name']) {
				data['name'] = data['title'];
			}

			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['content'] = data['content'] ? '<div>' + data['content'] + '</div>' : '';
			data['filename'] = data['filename'] ? data['filename'] : data['file'];
			data['startTemplateUrl'] = data['startTemplateUrl'] ? data['startTemplateUrl'] : '';
			e.preventDefault();

			return Vvveb.Builder.save(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;
		}
	},

	/**
	 * Opens the add-section box with the sections tab active.
	 *
	 * @param {Event} e - Event.
	 * @param {boolean} [onlyBase=false] - Show only base sections.
	 */
	newSection: function (e, onlyBase = false) {

		const addSectionBox = document.getElementById("add-section-box");

		//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;

	},

	/**
	 * Toggles designer mode based on the button's aria-pressed attribute.
	 */
	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
	/**
	 * Generic panel toggle using CSS custom properties.
	 *
	 * @param {string} panel - CSS selector.
	 * @param {string} cssVar - CSS custom property name.
	 * @returns {boolean} Whether the panel is now visible.
	 */
	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;
	},

	/**
	 * Toggles the file manager panel.
	 */
	toggleFileManager: function () {
		Vvveb.Gui.togglePanel("#filemanager", "--builder-filemanager-height");
	},

	/**
	 * Toggles the left panel.
	 */
	toggleLeftColumn: function () {
		Vvveb.Gui.togglePanel("#left-panel", "--builder-left-panel-width");
	},

	/**
	 * Toggles the right panel.
	 *
	 * @param {Event} e - Event.
	 * @param {boolean} [rightColumnEnabled=null] - Override for panel state.
	 */
	toggleRightColumn: function (e, rightColumnEnabled = null) {
		if (rightColumnEnabled === null) {
			rightColumnEnabled = Vvveb.Gui.togglePanel("#right-panel", "--builder-right-panel-width");
		} else {
			let panel = document.getElementById("right-panel");
			let body = document.querySelector("body");
			if (rightColumnEnabled) {
				panel.style.display = "none";
				body.style.setProperty("--builder-right-panel-width", "0px");
			} else {
				panel.style.display = "";
				body.style.setProperty("--builder-right-panel-width", "");
			}
		}

		if (rightColumnEnabled) {
			document.getElementById("vvveb-builder").classList.remove("no-right-panel");
			document.querySelector(".component-properties-tab").classList.add("d-none");
		} else {
			document.getElementById("vvveb-builder").classList.add("no-right-panel");
			document.querySelector(".component-properties-tab").classList.remove("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();
		}

	},

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

	/**
	 * Toggles the tree list between floating on the right and default position.
	 */
	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 = "auto";
			treeList.style.right = "0";
			treeList.style.top = "0";
			treeList.style.left = "auto";
			treeList.style.width = "300px";
			btnIcon.className = "icon-remove-outline";
		}
	},

	/**
	 * Moves the tree list between the canvas area and below the file manager.
	 */
	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";
		}
	},

	/**
	 * Applies a CSS scale transform to the iframe wrapper.
	 */
	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;
	},

	/**
	 * Sets the CSS state (hover, active, etc.) via StyleManager.
	 */
	setState: function () {
		Vvveb.StyleManager.setState(this.value);
		Vvveb.Builder.reloadComponent();
	},

	/**
	 * Sets the theme (light/dark) via StyleManager.
	 */
	setTheme: function () {
		Vvveb.StyleManager.setTheme(this.value);
		if (Vvveb.StyleManager.themeMode == "attribute") {
			Vvveb.Builder.documentFrame.contentDocument.querySelector("html").setAttribute("data-bs-theme", this.value); 
		}
		Vvveb.Builder.reloadComponent();
	}
}

/**
 * Manages CSS styles applied to elements in the iframe, including
 * media queries, themes, states, and CSS generation.
 *
 * @namespace StyleManager
 * @memberof Vvveb
 */
Vvveb.StyleManager = {

	/** @type {Object} Nested map: media → selector → property → value */
	styles: {},
	/** @type {Element|false} The <style id="vvvebjs-styles"> element in iframe head */
	cssContainer: false,
	/** @type {Object} Bootstrap breakpoint widths */
	breakpoints: {
		'sm': '575.98px',
		'md': '767.98px',
		'lg': '991.98px',
		'xl': '1199.98px',
		'xxl': '1399.98px'
	},
	/** @type {Document|boolean} The iframe document */
	doc: false,
	/** @type {boolean} Whether to set styles inline instead of in the stylesheet */
	inlineCSS: false,
	/** @type {Element|null} Cached current element for selector optimization */
	currentElement: null,
	/** @type {string|null} Cached CSS selector for currentElement */
	currentSelector: null,
	/** @type {string} Current active breakpoint name */
	currentBreakpoint: "none",
	/** @type {string} Current media query string */
	currentMedia: "none",
	/** @type {string} Current CSS state (e.g. "hover", "active", "") */
	state: "",//hover, active etc
	/** @type {string} Theme application mode: "attribute" or "media" */
	themeMode: "attribute",//attribute, media

	/**
	 * Initializes the style manager. Creates or finds the vvvebjs-styles element.
	 *
	 * @param {Document} [doc] - The iframe document.
	 * @returns {Element|undefined} The cssContainer style element.
	 */
	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;
		}
	},

	/**
	 * Parses all CSS rules from the stylesheet into the styles map.
	 */
	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 = style.cssRules[j].media?.mediaText ??  "none";

			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;
					}
				}
			}
		}
	},

	/**
	 * Builds a CSS selector for an element by walking up the DOM tree.
	 *
	 * @param {Element} element - DOM element.
	 * @returns {string} CSS selector string.
	 */
	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;
		}

		let selectorText = selector.reverse().join(" > ");

		if (this.themeMode == "attribute" && (this.theme == "light" || this.theme == "dark")) {
			selectorText = `[data-bs-theme="${this.theme}"] ` + selectorText;
		}
			
		return selectorText;
	},

	/**
	 * Sets the current CSS state.
	 *
	 * @param {string} state - State name (e.g. "hover").
	 */
	setState: function (state) {
		this.state = state;
	},

	/**
	 * Sets the current theme and updates the media query.
	 *
	 * @param {string} [theme=null] - "light", "dark", or null.
	 */
	setTheme: function (theme = null) {//light, dark, null
		this.theme = theme;
		this.setMedia();
	},

	/**
	 * Sets the current media query from breakpoint and theme.
	 *
	 * @param {string} [media=null] - Media query string.
	 */
	setMedia: function (media = null) {
		if (!media) {
			media = "";
			
			if (this.themeMode == "media" && (this.theme == "light" || this.theme == "dark")) {
				media = `(prefers-color-scheme: ${this.theme})`;
			}

			if (this.currentBreakpoint != "none") {
				if (media) {
					media += " and ";
				}
				
				let width = this.breakpoints[this.currentBreakpoint];
				media += `(max-width: ${width})`;
			}
		}

		if (!media) {
			media = "none";
		}
		
		this.currentMedia = media;
	},
	
	/**
	 * Appends the current state pseudo-class to a selector.
	 *
	 * @param {string} selector - Base CSS selector.
	 * @returns {string} Selector with state appended.
	 */
	addSelectorState: function (selector) {
		return selector + (this.state ? ":" + this.state : "");
	},

	/**
	 * Sets a CSS style on an element, updating the styles map and stylesheet.
	 *
	 * @param {Element|string} element - Target element or CSS selector.
	 * @param {string} styleProp - CSS property name.
	 * @param {string} value - CSS property value.
	 * @returns {Element} The element.
	 */
	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.currentMedia;

		//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);
	},

	/**
	 * Sets the CSS container innerHTML and reloads the styles map.
	 *
	 * @param {string} css - Raw CSS text.
	 */
	setCss: function (css) {
		this.cssContainer.innerHTML = css;
		this.loadCss();
	},

	/**
	 * Returns the current CSS content from the style element.
	 *
	 * @returns {string} Raw CSS text.
	 */
	getCss: function (css) {
		return this.cssContainer.innerHTML;
	},

	/**
	 * Generates a full CSS string from the styles map and writes to the stylesheet.
	 *
	 * @returns {string} The generated CSS text.
	 */
	generateCss: function () {
		//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 (const media in this.styles) {
			let indent = "";
			if (media !== "none") {
				let width = this.breakpoints[media];
				css += `@media ${media} {\n\n`;
				indent = `\t`;
			}
			for (const selector in this.styles[media]) {
				css += indent + `${selector} {\n`;
				for (const property in this.styles[media][selector]) {
					const value = this.styles[media][selector][property];
					css += indent + `\t${property}: ${value};\n`;
				}
				css += indent + '}\n\n';
			}
			if (media !== "none") {
				css += `}\n\n`;
			}
		}

		return this.cssContainer.innerHTML = css;
	},


	/**
	 * Reads a CSS property value from inline style, styles map, or computed style.
	 *
	 * @param {Element} element - DOM element.
	 * @param {string} styleProp - CSS property name.
	 * @returns {string} The CSS property value.
	 */
	_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.currentMedia;

		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;
	},

	/**
	 * Public wrapper for _getCssStyle.
	 *
	 * @param {Element} element - DOM element.
	 * @param {string} styleProp - CSS property name.
	 * @returns {string} The CSS property value.
	 */
	getStyle: function (element, styleProp) {
		return this._getCssStyle(element, styleProp);
	}
}

/**
 * Provides get/set methods for element content (attributes, HTML, text).
 *
 * @namespace ContentManager
 * @memberof Vvveb
 */
Vvveb.ContentManager = {
	/**
	 * Gets an attribute value from an element.
	 *
	 * @param {Element} element - Target element.
	 * @param {string} attrName - Attribute name.
	 * @returns {string} Attribute value.
	 */
	getAttr: function (element, attrName) {
		return element.getAttribute(attrName);
	},

	/**
	 * Sets an attribute value on an element.
	 *
	 * @param {Element} element - Target element.
	 * @param {string} attrName - Attribute name.
	 * @param {string} value - Attribute value.
	 */
	setAttr: function (element, attrName, value) {
		return element.setAttribute(attrName, value);
	},

	/**
	 * Sets innerHTML or outerHTML on an element.
	 *
	 * @param {Element} element - Target element.
	 * @param {string} html - HTML string.
	 * @param {boolean} [outer=false] - Whether to set outerHTML.
	 * @returns {Element} The element.
	 */
	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;
	},

	/**
	 * Gets innerHTML or outerHTML from an element.
	 *
	 * @param {Element} element - Target element.
	 * @param {boolean} [outer=false] - Whether to get outerHTML.
	 * @returns {string} HTML content.
	 */
	getHtml: function (element, outer = false) {
		if (outer) {
			return element.outerHTML;
		} else {
			return element.innerHTML;
		}
	},

	/**
	 * Sets innerText on an element.
	 *
	 * @param {Element} element - Target element.
	 * @param {string} text - Text content.
	 */
	setText: function (element, text) {
		return element.textContent = text;
	},

	/**
	 * Gets innerText from an element.
	 *
	 * @param {Element} element - Target element.
	 * @returns {string} Text content.
	 */
	getText: function (element) {
		return element.textContent;
	},
};

/**
 * Builds a hierarchical tree data structure by traversing DOM nodes.
 * Matches each node against registered Vvveb components.
 *
 * @param {Node} node - Root node to start traversal from.
 * @param {Array} parent - Array to push tree node objects into.
 * @param {Array|Object} allowedComponents - Whitelist of allowed component types.
 * @param {Object} [idToNode={}] - Output map of string IDs to DOM nodes.
 */
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;
				let matchChild;
				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');
}

/**
 * Converts a tree data structure into an HTML <ol> list with nested <li> items.
 *
 * @param {Array} tree - Array of tree node objects from getNodeTree.
 * @returns {HTMLDivElement} Root <ol> element containing the rendered tree.
 */
function drawComponentsTree(tree) {
	let j = 1;
	let prefix = Math.floor(Math.random() * 100);

	function drawComponentsTreeTraverse(tree) {
		let list = document.createElement("ol");
		j++;

		for (const 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.substring(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;

/**
 * Manages the page sections list in the editor sidebar.
 * Provides section navigation, reordering, adding, and deleting.
 *
 * @namespace SectionList
 * @memberof Vvveb
 */
Vvveb.SectionList = {

	/** @type {string} CSS selector for the sections container */
	selector: '.sections-container',
	/** @type {Object} Map of allowed component types */
	allowedComponents: {},

	/**
	 * Initializes the section list with click, drag, and hover event handlers.
	 *
	 * @param {Object} [allowedComponents={}] - Allowed component types filter.
	 */
	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 = element.parentNode._node;
				node.style.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 = element.parentNode._node;
				node.style.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();
			}
		});

	},

	/**
	 * Queries the frame document for top-level section elements.
	 *
	 * @returns {Array<Object>} Array of {name, id, type, node} objects.
	 */
	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;
	},

	/**
	 * Loads the component tree for a section item.
	 *
	 * @param {Element} sectionListItem - The section list item element.
	 * @param {Element} section - The actual DOM section node.
	 * @param {Object} [allowedComponents={}] - Allowed components filter.
	 */
	loadComponents: function (sectionListItem, section, allowedComponents = {}) {

		let tree = [];
		getNodeTree(section, tree, allowedComponents);

		let html = drawComponentsTree(tree);
		document.querySelector("ol", sectionListItem).replaceWith(html);
	},


	/**
	 * Creates a new section list item from a template and appends it.
	 *
	 * @param {Object} data - Data object with node property and template data.
	 */
	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);
	},

	/**
	 * Clears and rebuilds the entire sections list.
	 */
	loadSections: function () {
		let sections = this.getSections();
		let container = document.querySelector(this.selector);

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

	},

	//drag and drop 
	/**
	 * Drag-and-drop handler for dragging over section items.
	 *
	 * @param {DragEvent} e - The dragover event.
	 */
	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");
			}
		}
	},

	/**
	 * Drag-and-drop end handler. Reorders sections and records undo.
	 *
	 * @param {DragEvent} e - The dragend event.
	 */
	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;
		}
	},

	/**
	 * Drag-and-drop start handler.
	 *
	 * @param {DragEvent} e - The dragstart event.
	 */
	dragStart: function (e) {
		let element = e.target.closest("div");
		if (element) {
			selected = e.target
		}
	},
}

/**
 * Manages theme style switching for the editor.
 *
 * @namespace StyleList
 * @memberof Vvveb
 */
Vvveb.StyleList = {

	/** @type {string} CSS selector for the styles container */
	selector: '.sections-container',
	/** @type {Object} Allowed component types */
	allowedComponents: {},

	/**
	 * Applies or removes a theme stylesheet in the frame document.
	 *
	 * @param {string|falsy} style - URL of the CSS stylesheet, or falsy to remove.
	 */
	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;
	},

	/**
	 * Loads the currently active style and marks it in the UI.
	 *
	 * @param {Document} doc - The document (unused).
	 */
	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");
			}
		}
	},

	/**
	 * Binds click handler on .styles-list to toggle active styles.
	 */
	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();
			}
		});

	}
}

/**
 * Floating panel showing a navigable tree of all components in the page.
 *
 * @namespace TreeList
 * @memberof Vvveb
 */
Vvveb.TreeList = {
	/** @type {string} CSS selector for the tree list panel */
	selector: '#tree-list',

	/** @type {Element|null} The container DOM element */
	container: null,

	/** @type {Array} Current component tree data structure */
	tree: [],

	/** @type {Object} Map of string IDs to DOM nodes */
	idToNode: {},

	/**
	 * Initializes the tree list panel with drag-to-move, click, and mouseover handlers.
	 */
	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] label");
			if (element) {
				const node = element.parentNode._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,
				}));
			}
		})
	},

	/**
	 * Highlights a component in the tree list and scrolls it into view.
	 *
	 * @param {Element} node - The DOM node to select.
	 * @returns {boolean} Always returns false.
	 */
	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;
	},

	/**
	 * Rebuilds the entire component tree from the frame document.
	 */
	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);
	},
}

/**
 * Manages the file/page tree in the editor sidebar.
 * Handles page loading, renaming, duplicating, deleting, and URL history.
 *
 * @namespace FileManager
 * @memberof Vvveb
 */
Vvveb.FileManager = {
	/** @type {Element|boolean} The <ol> tree element or false */
	tree: false,
	/** @type {Object} Map of page names to page data objects */
	pages: {},
	/** @type {string|boolean} Current page name or false */
	currentPage: false,
	/** @type {Object} Allowed component types for filtering */
	allowedComponents: {},

	/**
	 * Initializes the file manager with click handlers for navigation,
	 * delete, rename, duplicate, and page loading.
	 *
	 * @param {Object} [allowedComponents={}] - Allowed components filter.
	 */
	init: function (allowedComponents = {}) {

		this.allowedComponents = allowedComponents;
		this.tree = document.querySelector("#filemanager .tree > ol");
		this.tree.replaceChildren();

		this.tree.addEventListener("click", function (e) {
			let element = e.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 = e.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 = e.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 = e.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 = e.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 = e.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 = e.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");
			}
		});
	},

	/**
	 * Resets the file manager state.
	 */
	clear: function () {
		this.pages = {};
		this.currentPage = false;
		this.tree.replaceChildren();
	},

	/**
	 * Deletes a page after confirmation via POST request.
	 *
	 * @param {Element} element - The <li> element representing the page.
	 * @param {Event} e - The click event.
	 */
	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}"?`)) {

			let detail = this.pages[page.page];
			detail['element'] = element;

			//allow event to change page or cancel by setting page to false
			window.dispatchEvent(new CustomEvent("Vvveb.FileManager.deletePage", {
				detail
			}));

			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);

						if (typeof error?.text === 'function') error.text().then(errorMessage => {
							let message = errorMessage.substring(0, 200);
							displayToast("danger", "Error", message);
						})
					});

				element.remove();
			}
		}
	},

	/**
	 * Renames or duplicates a page via POST request.
	 *
	 * @param {Element} element - The <li> element representing the page.
	 * @param {Event} e - The click event.
	 * @param {boolean} [duplicate=false] - If true, duplicates instead of renaming.
	 */
	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) {
			let detail = { page, newfile, element };
			//allow event to change page or newfile or cancel by setting page to false
			window.dispatchEvent(new CustomEvent("vvveb.FileManager.renamePage", {
				detail
			}));

			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);

						if (typeof error?.text === 'function') error.text().then(errorMessage => {
							let message = errorMessage.substring(0, 200);
							displayToast("danger", "Error", message);
						})
					});
			}
		}
	},

	/**
	 * Adds a page to the file tree.
	 *
	 * @param {string} name - Page identifier/name.
	 * @param {Object} data - Page data (url, file, title, folder, etc.).
	 * @param {string|boolean} [afterPage=false] - Page name to insert after.
	 * @param {boolean} [append=true] - Whether to append or prepend.
	 * @returns {Element|boolean} The created <li> element, or false.
	 */
	addPage: function (name, data, afterPage = false, append = true) {
		let detail = { name, data };
		//allow event to change name or cancel by setting name to false
		window.dispatchEvent(new CustomEvent("Vvveb.FileManager.addPage", {
			detail
		}));

		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;
	},

	/**
	 * Adds multiple pages to the file tree.
	 *
	 * @param {Object} pages - Map of page names to page data.
	 */
	addPages: function (pages) {
		for (const page in pages) {
			this.addPage(pages[page]['name'], pages[page]);
		}
	},

	/**
	 * Appends a component entry to a page's child list.
	 *
	 * @param {string} name - Component name.
	 * @param {string} url - Component URL.
	 * @param {string} title - Component title.
	 * @param {string} page - Page identifier.
	 */
	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 }));
	},

	/**
	 * Loads and draws the component tree for the current page.
	 *
	 * @param {Object} [allowedComponents={}] - Allowed components filter.
	 */
	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);
	},

	/**
	 * Returns the URL of the currently loaded page.
	 *
	 * @returns {string|undefined} The page URL.
	 */
	getCurrentUrl: function () {
		if (this.currentPage) {
			return this.pages[this.currentPage]['url'];
		}
	},

	/**
	 * Returns the name of the currently loaded page.
	 *
	 * @returns {string|boolean} The current page name, or false.
	 */
	getCurrentPage: function () {
		return this.currentPage;
	},

	/**
	 * Returns data for the current page.
	 *
	 * @param {string} [key] - Property key to retrieve.
	 * @returns {*} The requested property or full page data.
	 */
	getPageData: function (key) {
		if (this.currentPage) {
			if (key) {
				return this.pages[this.currentPage][key];
			} else {
				return this.pages[this.currentPage];
			}
		}
	},


	/**
	 * Returns the full file path for the current page.
	 *
	 * @returns {string|undefined} Full file name path.
	 */
	getCurrentFileName: function () {
		if (this.currentPage) {
			let folder = this.pages[this.currentPage]['folder'];
			folder = folder ? folder + '/' : '';
			return folder + this.pages[this.currentPage]['file'];
		}
	},

	/**
	 * Reloads the currently loaded page.
	 */
	reloadCurrentPage: function () {
		if (this.currentPage)
			return this.loadPage(this.currentPage);
	},

	/**
	 * Loads a page by name, updates UI, and dispatches events.
	 *
	 * @param {string} name - Page identifier to load.
	 * @param {boolean|Object} [allowedComponents=false] - Allowed components filter.
	 * @param {boolean} [disableCache=true] - Whether to append cache-busting parameter.
	 * @param {boolean} [loadComponents=false] - Whether to load components on load.
	 */
	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();
				});
		}
	},

	/**
	 * Smoothly scrolls a page element into view.
	 *
	 * @param {Element} page - DOM element to scroll to.
	 */
	scrollToPage: function (page) {
		page.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
	},
}

/**
 * Manages breadcrumb navigation showing the ancestor chain of the selected element.
 *
 * @namespace Breadcrumb
 * @memberof Vvveb
 */
Vvveb.Breadcrumb = {
	/** @type {Element|boolean} The breadcrumb container element or false */
	tree: false,

	/**
	 * Initializes the breadcrumb container with click and mousemove handlers.
	 */
	init: function () {
		this.tree = document.querySelector(".breadcrumb-navigator > .breadcrumb");
		this.tree.replaceChildren();

		this.tree.addEventListener("click", function (e) {
			let element = e.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 (e.target == currentHoverNode) return;
			currentHoverNode = e.target;

			let element = e.target.closest(".breadcrumb-item");
			if (element) {
				let node = element._node;

				node.dispatchEvent(new MouseEvent("mousemove", {
					bubbles: true,
					cancelable: true,
				}));
			}
		})
	},

	/**
	 * Creates a breadcrumb item and prepends it to the breadcrumb.
	 *
	 * @param {Object} data - Template data (name, className).
	 * @param {Element} element - The DOM node this breadcrumb represents.
	 */
	addElement: function (data, element) {
		let li = generateElements(tmpl("vvveb-breadcrumb-navigaton-item", data))[0];
		li._node = element;
		this.tree.prepend(li);
	},

	/**
	 * Rebuilds the breadcrumb for a given element by walking up parentElement.
	 *
	 * @param {Element} element - The element whose ancestor chain to display.
	 */
	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;
		}
	}
}


/**
 * Manages page revision history: load, display, restore, and delete revisions.
 *
 * @namespace Revisions
 * @memberof Vvveb
 */
Vvveb.Revisions = {

	/**
	 * Populates the revisions dropdown UI with revision items.
	 *
	 * @param {Object} revisions - Map of revision data objects.
	 */
	updateRevisionsDropdown: function (revisions) {
		let dropdown = document.querySelector(".revisions-dropdown");
		let count = Object.keys(revisions).length;
		document.querySelector(".revisions-count").innerHTML = count;
		dropdown.replaceChildren();

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

	/**
	 * Fetches revisions from the server for a given template.
	 *
	 * @param {string|falsy} template - Template file name.
	 */
	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.error(error);
					displayToast("danger", "Error", "Error loading revision!");
				});
		}
	},

	/**
	 * Initializes revision management. Listens for save/load events
	 * and binds click handlers for loading and deleting revisions.
	 */
	init: function () {
		let self = this;

		window.addEventListener("vvveb.Builder.save", 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.error(error);
							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.error(error);
								displayToast("danger", "Error", "Error deleting revision!");
							});
					}
				}
			}
		});
	}
}

/**
 * Handles server-side rendering of components via AJAX.
 * Sends component HTML to the server and replaces with the rendered response.
 *
 * @namespace Server
 * @memberof Vvveb
 */
Vvveb.Server = {
	/** @type {string} URL for AJAX requests */
	url: "",
	/** @type {string} Current component name being rendered */
	component: "",
	/** @type {number} Index of the component element in DOM query results */
	index: 0,
	/** @type {boolean} Whether to fully replace the element or just innerHTML */
	fullUpdate: false,
	/** @type {boolean} Whether to use server-side templates */
	userServerTemplate: false,

	/**
	 * Sends a POST request with component content and replaces the element
	 * with the server-rendered response.
	 *
	 * @param {Function} [callback] - Called with response text on success.
	 */
	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.error(error);
			});
	},

	//render a single component
	/**
	 * Renders a single server-side component. Extracts component name from
	 * data-v-component-* attributes and sends to server for rendering.
	 *
	 * @param {Element} element - Component DOM element.
	 * @param {string} [component] - Component name (currently unused).
	 * @param {number} [index] - Component index (auto-detected if undefined).
	 * @returns {Element} The original element.
	 */
	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);
		}*/

		this.component = null;
		for (const attribute of element.attributes) {
			if (attribute.name.startsWith("data-v-component-")) {
				this.component = attribute.name.replace("data-v-component-", "");
				break;
			}
		}

		if (!this.component) return element;

		this.index = index;
		if (typeof index == "undefined" || index == null) {
			let selector = "[data-v-component-" + this.component + "]";
			this.index = Array.prototype.indexOf.call(Vvveb.Builder.frameBody.querySelectorAll(selector), element);
		}

		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;
			clearTimeout(this.throttle);
			this.throttle = setTimeout(function () {
				self.ajax();
			}, 500);
		}

		return element;
	},

	//search for components inside element and render them all
	renderComponents(element) {
		const path = ".//*[ @*[starts-with(name(), 'data-v-component-')] ] | ../*[ @*[starts-with(name(), 'data-v-component-')] ]";
		const elements = window.FrameDocument.evaluate(path, element ?? window.FrameDocument, null, XPathResult.ANY_TYPE, null);

		const components = [];
		let currentComponent = elements.iterateNext();
		while (currentComponent) {
			components.push(currentComponent);
			currentComponent = elements.iterateNext();
		}

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

		return components;
	}
}

/**
 * Manages font providers, tracks active/used fonts, and provides font list population.
 *
 * @namespace FontsManager
 * @memberof Vvveb
 */
Vvveb.FontsManager = {

	/** @type {Array<Object>} Array of {provider, fontFamily, element} objects */
	activeFonts: [],
	/** @type {Object} Map of provider name to provider object */
	providers: {},//{"google":GoogleFontsManager};

	/**
	 * Adds a group of fonts from a provider to the UI dropdown.
	 *
	 * @param {string} provider - Font provider identifier.
	 * @param {string} groupName - Display name for the font group.
	 * @param {Object} fontList - Map of font name to font data.
	 */
	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;
		}
	},

	/**
	 * Registers a font provider object.
	 *
	 * @param {string} provider - Provider name.
	 * @param {Object} Obj - Provider object with addFont/removeFont methods.
	 */
	addProvider: function (provider, Obj) {
		this.providers[provider] = Obj;
	},

	//add also element so we can keep track of the used fonts to remove unused ones
	/**
	 * Adds a font via the appropriate provider and tracks it.
	 *
	 * @param {string} provider - Font provider name.
	 * @param {string} fontFamily - font-family CSS value.
	 * @param {Element} [element=false] - DOM element the font was applied to.
	 */
	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 });
		}
	},

	/**
	 * Removes a font via the appropriate provider.
	 *
	 * @param {string} provider - Font provider name.
	 * @param {string} fontFamily - font-family CSS value to remove.
	 */
	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
	/**
	 * Removes tracked fonts that no longer match their element's actual font-family.
	 */
	cleanUnusedFonts: function () {
		for (const 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);
				}
			}
		}
	}
};


/**
 * Simple key-value store for named colors used in the color picker/palette.
 *
 * @namespace ColorPalette
 * @memberof Vvveb
 */
Vvveb.ColorPalette = {
	/** @type {Object.<string, string>} Map of color names to color values */
	colors: {},

	/**
	 * Returns the entire colors map.
	 *
	 * @returns {Object.<string, string>} The colors map.
	 */
	getAll: function () {
		return this.colors;
	},

	/**
	 * Adds a color entry to the palette.
	 *
	 * @param {string} name - Color name/key.
	 * @param {string} color - Color value.
	 */
	add: function (name, color) {
		this.colors[name] = color;
	},

	/**
	 * Removes a color entry from the palette.
	 *
	 * @param {string} color - Color name/key to remove.
	 */
	remove: function (color) {
		delete this.colors[color];
	},
}

/**
 * Converts a CSS variable or kebab-case name into a human-readable string.
 * Strips --bs- prefix and replaces hyphens/underscores with spaces.
 *
 * @param {string} name - The raw name to format.
 * @returns {string} The formatted friendly name.
 */
function friendlyName(name) {
	name = name.replaceAll("--bs-", "").replace(/[-_]/g, " ").trim();
	return name = name[0].toUpperCase() + name.slice(1);
}

/**
 * Extracts and manages CSS custom properties from stylesheets,
 * categorizing them by type (font, color, dimensions).
 *
 * @namespace ColorPaletteManager
 * @memberof Vvveb
 */
Vvveb.ColorPaletteManager = {

	/** @type {Object} Map of type names to CSS variable metadata */
	cssVars: { "font": {}, "color": {}, "dimensions": {} },

	/**
	 * Returns the CSS variables map for a given type.
	 *
	 * @param {string} type - Variable type ("font", "color", or "dimensions").
	 * @returns {Object} Map of variable names to metadata.
	 */
	getType: function (type) {
		return this.cssVars[type];
	},

	/**
	 * Scans stylesheets for CSS custom properties and categorizes them by type.
	 *
	 * @param {CSSStyleSheetList} [styleSheets=document.styleSheets] - Stylesheets to scan.
	 * @param {string} [selector] - Optional selector filter.
	 * @returns {Object} The cssVars map.
	 */
	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') || value.endsWith('px')) {
									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;
	},

	/**
	 * Scans stylesheets for button CSS variables and maps them to theme variables.
	 *
	 * @param {CSSStyleSheetList} [styleSheets=document.styleSheets] - Stylesheets to scan.
	 * @param {Object} vars - Map of variable names to values.
	 * @returns {Object} Map of button variable names to theme variable names.
	 */
	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 (let 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;
	},

	/**
	 * Initializes the color palette manager. Renders the Bootstrap config component
	 * and adds valid hex colors to Vvveb.ColorPalette.
	 *
	 * @param {Document} document - The document context.
	 */
	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);
			}
		}
	},

};

/**
 * Manages the "New Section" placeholder at the bottom of the page in the editor.
 *
 * @namespace NewSection
 * @memberof Vvveb
 */
Vvveb.NewSection = {
	/** @type {Element|null} The new section placeholder DOM element */
	container: null,
	/** @type {string} HTML template for the placeholder */
	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>`,

	/**
	 * Initializes the new section widget with mouse and click handlers.
	 */
	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);
				}
			}
		};
	},

	/**
	 * Inserts the new section placeholder into the iframe.
	 *
	 * @returns {Element} The inserted container element.
	 */
	insert: function () {
		if (Vvveb.Builder.frameDoc.getElementById("vvvebjs-new-section")) return;
		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;
	}
}

/**
 * Configuration holder with stubs for loading config from JSON.
 *
 * @namespace Config
 * @memberof Vvveb
 */
Vvveb.Config = {
	/** @type {Array} Array of component configurations */
	components: [],
	/** @type {Array} Array of block configurations */
	blocks: [],
	/** @type {Array} Array of plugin configurations */
	plugins: [],

	/**
	 * Fetches a JSON configuration file from the given URL.
	 *
	 * @param {string} [url='default.json'] - URL of the JSON config file.
	 */
	load: function (url = 'default.json') {
		fetch(url)
			.then(response => response.json())
			.then(data => {
			});
	}
}

// Toggle fullscreen
/**
 * Toggles fullscreen mode for a given document with cross-browser support.
 *
 * @param {Document} document - The document to toggle fullscreen on.
 */
function launchFullScreen(document) {
	const el = document.documentElement;

	if (document.fullscreenElement) {
		document.exitFullscreen();
	} else if (el.requestFullscreen) {
		el.requestFullscreen();
	} else if (el.webkitRequestFullscreen) {
		el.webkitRequestFullscreen();
	}
}

/** @type {Array<Object>} Default font list with name/value pairs */
let fontList = [{
	value: "",
	text: "Default"
}, {
	value: "Arial, Helvetica, sans-serif",
	text: "Arial"
}];


window.addEventListener("popstate", checkState);

/**
 * Browser popstate event handler. Loads the page from state on back/forward navigation.
 *
 * @param {PopStateEvent} e - The popstate event.
 */
function checkState(e) {
	if (e.state && e.state.name) {
		Vvveb.FileManager.loadPage(e.state.name);
	}
}
